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.
48 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-27 |
Open Questions
Questions are organized by theme. Each question has a stable OQ-ID for cross-referencing from spec documents.
Door type classifications follow ADR-009:
- One-way door: Reversal requires rewriting significant code or permanently closes a capability. Requires ADR before implementation.
- Two-way door: Reversal is cheap or additive. Can be decided during implementation.
Theme: Core Types
OQ-01: BiStream Type Definition
- Origin: overview.md
- Status: resolved
- Door type: One-way
- Priority: high
- Resolution: BiStream is a trait (
AsyncRead + AsyncWrite + Send + Unpin). Handlers receive aConnection(not a single BiStream). This preserves the WASM door — browser clients can implement BiStream over WebTransport streams. See ADR-007. - Cross-references: ADR-002, ADR-007, ADR-009
OQ-02: AuthContext Resolution Timing
- Origin: overview.md
- Status: resolved
- Door type: One-way
- Priority: high
- Resolution: Hybrid model (Option C) — endpoint resolves what it can (e.g., TLS client certificate), handler resolves what it must (e.g., AuthToken in first frame). AuthContext may be partial when
handle()is called. See ADR-004. - Cross-references: ADR-002, ADR-004
Theme: ALPN and Routing
OQ-03: ALPN String Naming Convention
- Origin: overview.md
- Status: resolved
- Door type: One-way
- Priority: medium
- Resolution: Custom ALPNs use
alknet/<name>prefix (no version), standard ALPNs use IANA strings. No version negotiation initially. See ADR-006. - Cross-references: ADR-001, ADR-006
OQ-04: Dynamic Handler Registration at Runtime vs Static at Startup
-
Origin: overview.md
-
Status: resolved
-
Door type: Two-way
-
Priority: low
-
Resolution: Static registration at startup.
HandlerRegistryis immutable after construction. ALPN strings in the TLSServerConfigare derived from the registry at startup — adding a handler at runtime requires rebuilding the TLS config. TheArcSwap<HandlerRegistry>pattern can be applied later if needed (two-way door). See ADR-010.Scope clarification (ADR-024): This resolution applies to the
HandlerRegistry(ALPN string →ProtocolHandler), which is what ADR-010 governs. The call protocol'sOperationRegistry(operation name →HandlerRegistration) is a separate registry living inside theCallAdapter, behind the single ALPNalknet/call. Its mutability profile is governed by ADR-024, not by this OQ. ADR-024 layers the operation registry by trust boundary: curatedLocalops are immutable (same rationale as here — composing ops are privileged, the startup trust boundary is where their authority is granted);Sessionand imported (FromCalletc.) ops are dynamic at their respective trust-boundary scopes (session, connection). The pre-ADR-024 blanket immutability claim inoperation-registry.mdwas inherited by analogy from this OQ and did not actually apply — the TLS-config argument that justifiesHandlerRegistryimmutability does not touch theOperationRegistry. -
Cross-references: ADR-001, ADR-010, ADR-024, endpoint.md, operation-registry.md
Theme: Transport and Endpoint
OQ-05: Multi-Connectivity Endpoint
- Origin: overview.md
- Status: resolved
- Door type: One-way
- Priority: high
- Resolution:
AlknetEndpointsupports bothquinn::Endpoint(public QUIC+TLS) andiroh::Endpoint(P2P relay-assisted) simultaneously, both optional and feature-gated. Both produce QUIC connections that dispatch through the sameHandlerRegistryby ALPN string. These are not interchangeable transports — they serve fundamentally different deployment contexts (public IP vs NAT traversal). TCP is not an endpoint concern — bare TCP SSH is handled by the SSH handler directly. See ADR-010. - Cross-references: ADR-001, ADR-010, endpoint.md
OQ-06: Server-Side ALPN vs Client-Side ALPN
- Origin: ADR-001
- Status: resolved
- Door type: One-way
- Priority: low
- Resolution: One ALPN per connection. Clients open one QUIC connection per ALPN. QUIC connections are cheap (multiplexed over the same UDP flow). See ADR-006.
- Cross-references: ADR-001, ADR-006
Theme: Call Protocol
OQ-07: Call Protocol Scope Within a Connection
- Origin: ADR-005
- Status: resolved
- Door type: Two-way
- Priority: medium
- Resolution: The call protocol uses bidirectional QUIC streams with EventEnvelope framing and ID-based correlation via PendingRequestMap. The protocol is stream-agnostic — the client can open one stream per operation, multiplex on one stream, or any mix. Correlation is by request ID, not by stream. Both sides can initiate calls. One
alknet/callconnection gives access to the full operation registry (call, subscribe, batch, schema). No multiplexing layer is needed inside the connection. See ADR-012. - Cross-references: ADR-005, ADR-012
Theme: Security
OQ-08: Vault Integration Point
- Origin: overview.md
- Status: resolved
- Door type: One-way
- Priority: medium
- Resolution: CLI-embedded, assembly-layer only. The CLI binary instantiates
VaultServiceHandlelocally at startup, derives and decrypts the credentials each handler needs, and injects them into handler capabilities. alknet-vault has no ALPN, no alknet-core dependency, and no operations registered in the call protocol. The master seed and derived private keys never cross the network. The vault is a capability source, not a network service. See ADR-008 and ADR-014. - Cross-references: ADR-003, ADR-005, ADR-008, ADR-014
Deferred Questions
These questions are acknowledged but not active. They will be promoted to open when their crate is being specified.
OQ-09: WASM Target Boundaries
- Origin: overview.md
- Status: deferred
- Door type: One-way (when applicable)
- Priority: low
- Resolution: Not an active question — WASM compatibility is a design constraint (see ADR-009, overview.md design principles), not a deliverable. Specific WASM targeting decisions will be made when individual crates are implemented. BiStream being a trait preserves the client-side stream door — a browser can implement BiStream over WebTransport streams. The server-side dispatch door is NOT preserved by ADR-007 and is a known, accepted closure:
Connectionis a concrete quinn-bound struct (not a trait), the accept loop usestokio::spawn(tokio does not run on WASM), and the call-protocol dispatch internals (PendingRequestMap,CallAdapter) use tokiooneshot/mpscchannels. A WASM server-side peer would require aConnectiontrait and a runtime-abstracted accept loop — not planned. The browser path is client-side via a JS SDK, not server-side Rust-to-WASM. This is an explicit one-way door, not an oversight. - Cross-references: ADR-007, ADR-009
OQ-10: Git Adapter Scope — Smart Protocol Only or Full Server?
- Origin: overview.md
- Status: deferred
- Door type: Two-way
- Priority: low
- Resolution: Deferred per the cleanup plan. Start with git smart protocol over QUIC streams. ERC721 integration and full server capabilities are additive. Composability fork (review #002 W18): whether git operations are registered in the
OperationRegistryand callable viaenv.invoke(), or only available as raw smart protocol onalknet/git, is a separate decision from ERC721 scope. The path of least resistance (raw smart protocol only) forecloses agent composition of git operations — an agent handler that wants to composegit/clonecannot, because there's noOperationSpec, noHandler, no registration. To make git composable, a call-protocol projection (a set ofHandlerRegistrationbundles wrapping git operations behind the registry) must be built alongside or instead of the raw handler. Resolve this when speccing alknet-git, not deferred past it. - Cross-references: ADR-001
Theme: alknet-core
OQ-11: Handler-Level Auth Resolution Observability
-
Origin: auth.md
-
Status: resolved
-
Door type: Two-way
-
Priority: medium
-
Resolution: Option B — handlers store resolved identity on the Connection. When a handler resolves identity inside
handle()(the handler-level auth phase), it callsconnection.set_identity(identity)to store the resolvedIdentityon the connection object. The endpoint and observability layers can read it later for connection logging, audit trails, and metrics.Why not Option A (return identity from
handle()): it changes theProtocolHandlertrait signature for all handlers, even those that don't do auth resolution (DNS, health check). It also assumes one identity per connection — but the call protocol can have different identities per request on the same connection (one connection, multiplecall.requestedevents with different auth tokens). Returning a single identity fromhandle()would be misleading for the call protocol.Why not Option C (identity stays local): the resolved identity is useful beyond the handler. The endpoint may want to log "connection from X authenticated as Y." A connection-level observability layer needs the identity. If it stays local, every handler that resolves identity would need to duplicate logging logic, and the endpoint can't correlate connections to identities.
Two identity scopes exist and must not be conflated:
- Connection-level identity (this decision): set once by the handler in
handle(), stored onConnection, read by the endpoint for logging/observability. This is the "connection owner" — who opened this QUIC connection. - Per-request identity (already in the call protocol spec): set per
call.requestedby theCallAdapter, stored onOperationContext.identity. This is the "call caller" — who is making this specific call, which may upgrade mid-session (different auth tokens on the same connection).
Both exist. The connection-level identity is the stable "who is this connection from"; the per-request identity is the dynamic "who is this specific call from." The call protocol's per-request resolution (which may produce a different identity than the connection-level resolution) takes precedence for ACL on
OperationContext— the connection-level identity is for observability only, not for ACL.C13 resolution (review #002): the endpoint does not read
identity()afterhandle()returns. TheConnectionis moved into the spawned handler task (endpoint.md), so the endpoint no longer has a reference to it. Connection-level observability (remote addr, ALPN, connection ID) is logged by the endpoint before the move. Identity-level observability is logged by the handler (the handler knows which identity it resolved and can log it). There is noArc<Connection>sharing or channel-based identity-reporting mechanism — the simplest honest answer that avoids over-engineering the observability path before there's a demonstrated need. If a future use case requires the endpoint to correlate connections to identities, anArc<Connection>or a side-channel can be added then. - Connection-level identity (this decision): set once by the handler in
-
Cross-references: ADR-004, ADR-011, ADR-015 (per-request identity on OperationContext), auth.md
OQ-12: TLS Identity Provisioning in AlknetEndpoint
-
Origin: endpoint.md, config.md
-
Status: resolved
-
Door type: One-way
-
Priority: high
-
Resolution: TLS identity in alknet has two distinct use cases, not one:
Use case 1 — P2P / key-based identity (default for most alknet nodes): RFC 7250 raw Ed25519 public keys. No domain, no CA, no cert renewal. The Ed25519 public key IS the node's identity. This is the same model iroh uses with its
NodeId. It works natively with SSH auth (same key type) and git (SSH key-based auth).TlsIdentity::RawKey(Ed25519SecretKey)inStaticConfigcovers this. As of ADR-027,RawKeyusesed25519_dalek::SigningKey(via an alknet-core wrapper), notiroh::SecretKey— so raw-key TLS identity is available in quinn-only builds without theirohfeature.Use case 2 — Domain-hosted services (relays, public-facing nodes): X.509 certificates with domain names. Required for browser/WebTransport clients, which don't support RFC 7250. This has two sub-cases:
- Manual: Provide cert/key file paths via
TlsIdentity::X509. Already specified inStaticConfig. - ACME auto-provisioning: Let's Encrypt via
rustls-acme.TlsIdentity::Acme { domains, cache_dir, directory, contact }carries static config; the endpoint constructs theAcmeStateasync state machine at setup time. Feature-gated behindacme. Designed in ADR-027. The reverse-proxy project (/workspace/@alkdev/reverse-proxy) demonstrates the proven pattern:AcmeConfig,ResolvesServerCertAcme, TLS-ALPN-01 challenge handling, automatic renewal.
Browser constraint: Browsers require X.509 and don't support RFC 7250. For browser/WebTransport clients, domain-hosted nodes with X.509 certs are mandatory. All other clients (SSH, git, alknet-native) work with raw keys by default.
The
TlsIdentityenum inStaticConfigcaptures all four modes (X509,RawKey,SelfSigned,Acme). ADR-027 records the design decisions for ACME integration and RawKey decoupling. - Manual: Provide cert/key file paths via
-
Cross-references: ADR-010, ADR-027, config.md, endpoint.md
OQ-13: Operation Path Format and Routing Scope
- Origin: operation-registry.md
- Status: resolved
- Door type: Two-way
- Priority: medium
- Resolution: alknet-call uses
/{service}/{op}(e.g.,/fs/readFile,/agent/chat,/services/list). This is the correct format for the alknet-call crate — it is not a "Phase 1 simplification" but the right design for this architecture. The/{node}/{service}/{op}pattern from the reference implementation served a head/worker routing model that is a separate architectural concern. Remote dispatch (federation / node-level routing) would be a different mechanism at a different layer, not a prefix added to alknet-call's operation paths. If remote dispatch is ever needed, it would be addressed by a separate crate or a routing layer above the operation registry, not by changing alknet-call's path format. Two-way door — the path format can be extended later if needed, but/{service}/{op}is the correct design now. - Cross-references: ADR-005, ADR-012
OQ-14: Batch Operation Semantics
- Origin: call-protocol.md
- Status: resolved
- Door type: Two-way
- Priority: low
- Resolution: Batch is a client-side pattern — multiple
call.requestedevents with correlated IDs, responses arrive independently. This is the correct protocol design, not a simplification to be "upgraded" later. QUIC's stream multiplexing already provides the concurrency and ordering guarantees that batch would need. Batch-specific event types (e.g.,batch.requested,batch.responded) would add protocol complexity without clear benefit over sending multiplecall.requestedevents. If a compelling use case for atomic batch semantics emerges, it can be added as a new event type without breaking existing clients. Two-way door. - Cross-references: ADR-012
Theme: alknet-call
OQ-15: Call Protocol Client and Adapter Contract
- Origin: call-protocol.md, operation-registry.md, ADR-013
- Status: resolved
- Door type: One-way
- Priority: high
- Resolution:
CallClientopens QUIC connections and shares the dispatch loop withCallAdapter— both sides can send and receivecall.requestedonce connected. Connection direction (who opened the connection) is independent of call direction (who calls whom).from_calladapter discovers remote operations viaservices/list+services/schemaand registers them with forwarding handlers — same pattern asfrom_openapiandfrom_mcp.to_openapiandto_mcpproject local operations to external protocols. Adapter contract trait (OperationAdapter) produces(OperationSpec, Handler)pairs. Cross-node call tree: abort cascade (ADR-016) propagates across node boundaries throughfrom_callhandlers. Credentials for connections come from capabilities (ADR-014). Adapter-registered operations areInternalby default (ADR-015). See ADR-017. - Cross-references: ADR-005, ADR-013, ADR-014, ADR-015, ADR-016, ADR-017, call-protocol.md, operation-registry.md
OQ-16: Safe Vault Operations for Call Protocol Exposure
- Origin: operation-registry.md, ADR-008
- Status: resolved
- Door type: One-way
- Priority: high
- Resolution: No vault operations are exposed over the call protocol for now. The vault is accessed only at the assembly layer (CLI binary at startup). Handlers receive secret material through
OperationContext.capabilities, not by calling vault operations over the wire. Theoperation-registry.mdspec previously showedvault/derive,vault/unlock, andvault/decryptregistered as call protocol operations — that was a contradiction with ADR-008's "capability source" model and has been corrected. If a future use case requires exposing a vault operation over the call protocol (e.g., a restrictedvault/public-keyoperation that returns only public key material for identity verification), it would require its own ADR with an explicit threat model justification. See ADR-014. - Cross-references: ADR-008, ADR-014, operation-registry.md
OQ-17: Abort Cascade Semantics for Nested Calls
- Origin: call-protocol.md, operation-registry.md
- Status: resolved
- Door type: One-way (protocol schema), two-way (mechanism)
- Priority: high
- Resolution:
call.abortedcascades to all non-terminal descendants in the call tree. The CallAdapter walks the tree (indexed byparent_request_idinPendingRequestMap) and sendscall.abortedfor each descendant. Default policy isabort-dependents(abort everything downstream);continue-runningis an opt-in for long-running work that should survive a parent's abort. Handlers clean up via Rust's async drop semantics (future dropped →Dropguards release resources). The cascade is protocol-level (server discovers descendants and propagates); the mechanism (parent-indexed map, cancellation tokens, or a separate graph) is a two-way door. See ADR-016. - Cross-references: ADR-012, ADR-015, ADR-016, call-protocol.md, operation-registry.md
OQ-18: Privilege Model and Authority Context
- Origin: operation-registry.md
- Status: resolved
- Door type: One-way (ACL model), two-way (specific APIs)
- Priority: high
- Resolution: The
internalflag onOperationContextmarks calls that originated from composition (a handler calling another operation viaOperationEnv), as opposed to external calls that arrived ascall.requestedfrom a wire client. Theinternalflag switches the authority context: the ACL check runs against the composing handler's identity (set at registration), not the caller's identity and not as a blanket skip. This replaces the previoustrustedflag, which skipped ACL entirely — a privilege escalation vector. Operations have External/Internal visibility. Internal operations returnNOT_FOUNDwhen called from the wire and are excluded fromservices/list. The composition env is scoped — a handler can only invoke a declared set of operations. Handler identity is carried onOperationContextalongside caller identity (the principal/agent pair). See ADR-015. - Cross-references: ADR-014, ADR-015, call-protocol.md, operation-registry.md
OQ-19: Session-Scoped Operation Registries and Agent-Written Operations
-
Origin: operation-registry.md
-
Status: resolved
-
Door type: Two-way (protocol doesn't need changes), one-way (if implementation closes the door)
-
Priority: medium
-
Resolution: The call protocol supports session-scoped registries through
OperationEnvtrait layering. No protocol changes needed. The pattern is documented here and in operation-registry.md to prevent an implementation from accidentally closing it.The registry model has three tiers:
Tier Scope Lifetime Visibility Who populates it Core (global) All sessions Process lifetime, static at startup External + Internal (curated) Assembly layer at startup Session One session Session lifetime, dynamic Internal only (never wire-facing) Agent during session (sandbox) Promotion Session → Core One-time transition Manual/curated review Human or architect agent reviews, then redeploys Session-scoped operations are always
Internal(ADR-015), run under the handler's identity (the agent handler that authorized the sandbox), can only compose operations in the handler's scoped env, and are ephemeral (gone when the session ends). Core operations are curated — reviewed before promotion. The promotion path is the curation checkpoint where autonomous (session-scoped) becomes curated (core). This is not auto-promotion.Implementation guard:
OperationEnvmust remain a trait, not a concrete type. A session-scoped env wraps the global env (check session registry first, fall through to global). MakingOperationEnvconcrete or hardcoding the global registry into the dispatch path would close this pattern. The static registration constraint (OQ-04) applies to the curated (Layer 0) registry only; session registries are dynamic by nature and are a different registry overlaying the curated one. Generalized by ADR-024: connection-scoped remote imports (from_call) use the same overlay mechanism as session-scoped ops. Both are per-scope dynamic overlays on the static curated base, composed into the per-callOperationContext.envby theCallAdapter.OperationEnvbeing a trait object (Arc<dyn OperationEnv + Send + Sync>) is what enables both overlay patterns.Session-scoped operations run in a locked-down sandbox (no direct net/fs/env access), can only reach operations in the handler's scoped env, and their output should be validated against their declared schema before returning. The promotion path requires review — an agent with a
promotescope (the architect role) performs the promotion; the writing agent (lower-privileged role) requests it. This is the role-based escalation pattern (ADR-015): privileges escalate through a chain of command, not through direct authority.The agent-specific mechanism (quickjs sandbox, session registry lifecycle, promotion workflow) belongs to the agent crate spec. The call protocol's job is to keep the
OperationEnvtrait composable and the visibility/ACL model consistent across tiers. -
Cross-references: OQ-04, ADR-014, ADR-015, ADR-016, ADR-024, operation-registry.md
Theme: alknet-vault
OQ-20: Salt/KDF and Encryption Key Derivation Method
- Origin: encryption.md
- Status: resolved
- Door type: One-way (key derivation method), two-way (salt field usage)
- Priority: high
- Resolution: The vault uses SLIP-0010 HD derivation from the BIP39 seed at path
m/74'/2'/0'/0'to produce the AES-256-GCM encryption key — not PBKDF2. Thesaltfield inEncryptedDatais unused for key derivation (kept for wire-format compatibility with the TS predecessor). The TypeScript@alkdev/storagecrypto module used PBKDF2 with a password + salt; data encrypted by that method (key_version=1) cannot be decrypted by the vault and must be migrated via one-time re-encryption to key_version=2. See ADR-020 for the full rationale and migration path. - Cross-references: ADR-020, encryption.md
OQ-21: Remote Vault Administration
-
Origin: service.md, protocol.md, ADR-019
-
Status: resolved
-
Door type: One-way (vault crate is local-only by construction)
-
Priority: medium
-
Resolution: Remote vault access is not a feature of the vault crate. ADR-025 dropped irpc from the vault, making the vault local-only by construction — no
RemoteServicetrait, no wire format for vault messages, no default-insecure remote handler. The vault's API isVaultServiceHandle(direct method calls), nothing else.If remote vault access is ever needed (e.g., the machine→worker pattern), it requires a separate vault-server crate that depends on both alknet-core (for
IdentityProvider, scopes, auth-wrapping) and alknet-vault (forVaultServiceHandle). That crate would define its own threat model, access policy, operation filtering (Unlock/Lock local-only), and wire format — and requires its own ADR. This is a deliberate addition, not a flag flip on a default that was already loaded.The pre-ADR-025 deferral framed remote access as "non-breaking" (the wire format was additive). That framing was misleading: once workers build dependencies on the remote vault API, disabling it breaks them — the door is operationally one-way even if the wire format is additive. ADR-025 inverts the default: the vault is local-only by construction, and remote access requires building something new, not removing a default.
Per-node vaults are the recommended pattern for multi-node deployments: each node has its own vault and mnemonic; credentials are encrypted for the receiving node's public key, not decrypted centrally. This is end-to-end encryption between nodes, matching ADR-008's "capability source" model.
-
Cross-references: ADR-005, ADR-008, ADR-014, ADR-018, ADR-019, ADR-025, protocol.md, service.md
OQ-22: Key Rotation Mechanism
- Origin: encryption.md
- Status: resolved
- Door type: One-way (path scheme), two-way (rotation policy)
- Priority: medium
- Resolution: Key rotation uses version-indexed derivation paths. Each key version maps to a distinct SLIP-0010 path:
m/74'/2'/0'/{version-2}'. v2 (current) is atm/74'/2'/0'/0'; v3 is atm/74'/2'/0'/1'; etc. Thedecryptmethod derives the key at the path indicated byencrypted.key_version(not always atPATHS::ENCRYPTION). Therotatemethod decrypts with the old version's key and re-encrypts with the new version's key — no new mnemonic needed. The assembly layer or a migration tool iterates stored blobs and callsrotateon each; the vault does not self-rotate. Partial rotation is safe (old keys remain derivable). See ADR-021. - Cross-references: ADR-020, ADR-021, encryption.md, service.md
OQ-23: Handler Identity Registration Path and Composition Authority
- Origin: operation-registry.md, call-protocol.md, ADR-015
- Status: resolved
- Door type: One-way (security model), two-way (bundle shape)
- Priority: high
- Resolution: ADR-015 said handler identity was "set at registration by the assembly layer" but the registration API (
register(spec, handler)) had no place for it — meaning every internal call would check ACL againstNone, reproducing the escalation gap ADR-015 was written to close. ADR-022 resolves this with a registration bundle (HandlerRegistration) carryingprovenance,composition_authority(replacinghandler_identity: Identity— it's a declared authority bundle, not a peer identity),scoped_env, andcapabilities. The dispatch path (build_root_contextandOperationEnv::invoke()) reads from the bundle. Provenance determines which ops can compose: onlyLocalandSessionget composition authority; leaves (FromOpenAPI,FromMCP,FromCall) getNone— they don't compose, so they don't need it. Capabilities are per-request onOperationContext, populated from the bundle (resolving the closure-capture vs context ambiguity). The kernel/user analogy: user's authority checked once at the External gate; handler's composition authority used for all composition inside; scoped env bounds reachability. No intersection — the user's authority does not limit internal calls. See ADR-022. - Cross-references: ADR-014, ADR-015, ADR-022, docs/reviews/001-pre-implementation-architecture-sanity-check.md (C1–C4), operation-registry.md, call-protocol.md
OQ-24: Operation Error Schemas
- Origin: operation-registry.md, call-protocol.md, ADR-017
- Status: resolved
- Door type: One-way (wire format), two-way (mapping mechanism)
- Priority: high
- Resolution:
OperationSpecgainserror_schemas: Vec<ErrorDefinition>where eachErrorDefinitioncarries acode,description,schema(JSON Schema for the error detail payload), and optionalhttp_status(for adapter projection). Thecall.errorpayload gains an optionaldetailsfield carrying the typed error payload. Protocol-level codes (NOT_FOUND,FORBIDDEN,INVALID_INPUT,INTERNAL,TIMEOUT) are distinct from operation-level domain codes (FILE_NOT_FOUND,RATE_LIMITED, etc.) — protocol codes are emitted by the dispatch machinery, operation codes by handlers.from_openapi/to_openapimap OpenAPI response status codes to/fromErrorDefinitions, making the adapter contract from ADR-017 faithful on the error axis.services/schemaexposeserror_schemasfor client code generation. See ADR-023. - Cross-references: ADR-017, ADR-023, docs/reviews/001-pre-implementation-architecture-sanity-check.md (C5), operation-registry.md, call-protocol.md
Theme: Call Client and Adapters
These open questions are the remainders from the call-completion gap analysis
(docs/research/alknet-call-completion/gap-analysis.md, DC-1..4) and the
peer-graph routing research (docs/research/alknet-call-peer-routing/findings.md).
ADR-029 supersedes ADR-028 and dissolves OQ-25 and the cross-peer half of
OQ-28; the remaining two-way-door shape/defaults are recorded in
client-and-adapters.md and may be
revisited during implementation without a new ADR.
OQ-25: Remote-Safe Marking Shape for CallClient Peer-Scoped Filtering (Dissolved by ADR-029)
- Origin: client-and-adapters.md, ADR-017 (§1 Consequences), ADR-028
- Status: dissolved (ADR-029)
- Door type:
Two-way (shape only — existence is one-way, resolved by ADR-028) - Priority:
medium - Resolution: Dissolved by ADR-029.
ADR-028's
remote_safe: bool/trusted_peermodel is superseded — it was a parallel, weaker authorization system that duplicated the existingAccessControl/Identitymachinery. ADR-029 retiresremote_safe/trusted_peerentirely; peer authorization flows throughAccessControl::check(peer_identity). The op'sAccessControlis the peer-authorization policy — there is no separate marking. Per-peer differentiation is viaIdentityProviderconfig (different peers get different scopes), not a per-op boolean. The "shape" question is moot because there is no marking to shape. See ADR-029 §3. - Cross-references: ADR-009, ADR-014, ADR-015, ADR-017, ADR-022, ADR-024,
ADR-028(superseded), ADR-029, client-and-adapters.md, operation-registry.md
OQ-26: OperationAdapter Error Type (AdapterError Variants)
-
Origin: client-and-adapters.md, ADR-017 §5, ADR-029 §5
-
Status: resolved (2026-06-27)
-
Door type: Two-way
-
Priority: medium
-
Resolution: The
AdapterErrorenum is#[non_exhaustive]+thiserror::Error, with these v1 variants:DiscoveryFailed { message: String }—from_callremote unreachable /services/listfailedSchemaParse { message: String }—from_openapi/from_jsonschemacouldn't parse the specTransport { message: String }— underlying transport error (QUIC forfrom_call, HTTP forfrom_openapi/from_mcp)Unauthorized { message: String }— HTTP 401 forfrom_openapi/from_mcp, auth rejected forfrom_callSamePeerCollision { message: String }— namespace collision within a single peer (ADR-029 §5: cross-peer collision dissolves; same-peer collision stays an error). Replaces the flatConflictvariant from the pre-ADR-029 implementation.
#[non_exhaustive]letsalknet-http's adapters extend without breaking match arms. The variant payloads areStringmessages — kept simple andSend + Syncby construction. This matches the shipped implementation (crates/alknet-call/src/client/mod.rs) exceptConflict→SamePeerCollision(the ADR-029 migration renames it). Two-way door: adding variants later is non-breaking; renaming a variant is a match-arm update but not an architectural change. -
Cross-references: ADR-017, ADR-029, client-and-adapters.md
OQ-27: from_call Re-Import Trigger
- Origin: client-and-adapters.md, ADR-017 Assumption 4
- Status: open
- Door type: Two-way
- Priority: low
- Resolution: ADR-017 Assumption 4 noted re-import "happens on
reconnection or is triggered explicitly." The v1 default is
auto-re-import on connection establishment. The overlay is
per-connection (Layer 2, ADR-024), so a stale overlay dies with the
connection; re-import on reconnect is naturally scoped to the new
connection. This is the right default for the runner pattern (a worker
reconnects → the hub re-discovers the worker's ops automatically).
Explicit re-import via a future
CallConnection::refresh()method is additive and can be added if a deployment needs manual control. Reversal is cheap; no ADR needed. - Cross-references: ADR-017, ADR-024, client-and-adapters.md
OQ-28: from_call Namespace Collision Behavior
-
Origin: client-and-adapters.md, ADR-017 §3
-
Status: open
-
Door type: Two-way
-
Priority: low
-
Resolution: ADR-017 §3's
FromCallConfignamespace prefix is optional, default no prefix, collision = error. A node importing from two remotes that both expose/container/execwithout prefixes should fail loudly rather than silently overwrite. The operator adds prefixes when they know they're importing from multiple sources. This matches the default-deny, explicit-allow posture (ADR-015, ADR-028). Reversal is cheap; no ADR needed. The alternative (last-wins) would silently mask one remote's op behind another's, which is the kind of surprise the default-deny posture exists to avoid.Cross-peer collision dissolved by ADR-029. Under the peer-keyed overlay model, same name on different peers is fine — they live in separate peer sub-overlays, no collision, no prefix needed. The collision rule now stays only within a peer (same name on the same peer is still an error — a peer shouldn't expose two ops with the same name).
FromCallConfig::namespace_prefixbecomes optional local-naming sugar, not the disambiguation mechanism. See ADR-029 §5. -
Cross-references: ADR-015, ADR-017,
ADR-028(superseded), ADR-029, client-and-adapters.md
OQ-29: CallClient TLS Client-Auth and Remote-Identity Verification
- Origin: client-and-adapters.md, ADR-017 §7
- Status: open
- Door type: Two-way
- Priority: medium
- Resolution: v1
CallClient::connect()builds the quinn client config withwith_no_client_auth()and anAcceptAnyServerCertVerifier— the client does not present its TLS identity (credentials.tls_identity) as a client cert, and does not pin the remote's expected identity fromcredentials.remote_identity. The server-sideAcceptAnyCertVerifier(in alknet-core's endpoint) does not require or verify client certs, so a client cert is not needed to establish a connection in v1. Wiring the local node's RawKey/X509 identity as a rustls client-auth cert (for servers that do verify client identity) and pluggingcredentials.remote_identityinto a realServerCertVerifieris additive — a two-way-door remainder surfaced during implementation. The one-way constraint (credentials fromCapabilities, not env vars, ADR-014) is unaffected: theauth_tokendimension flows through the call-protocolauth_tokenpayload field, not TLS, so the no-env-vars invariant holds independently of this gap. Decided during a future task that wires RawKey client-auth; recorded here, not in a full ADR. - Cross-references: ADR-014, ADR-017, ADR-027, client-and-adapters.md, endpoint.md
OQ-30: PeerRef::Any Routing Policy
- Origin: ADR-029 §2, client-and-adapters.md,
docs/research/alknet-call-peer-routing/findings.md§3.2 - Status: open
- Door type: Two-way
- Priority: low
- Resolution: v1
PeerRef::Anyuses insertion-order first-match — deterministic but order-dependent (worker A connects before worker B →Anyroutes to A until A disconnects). This is the simplest routing policy and is correct for the immediate use case (the head picks the first worker that serves the op). A richerRoutingPolicy(round-robin, least-loaded, affinity) is the two-way-door remainder; thePeerRefenum is designed to compose with aRoute { selector, policy }struct without breaking theinvoke_peersignature. Decided during implementation when a fan-out use case needs it; recorded here, not in a full ADR. - Cross-references: ADR-029, client-and-adapters.md
OQ-31: services/list-peers Re-Export Semantics
- Origin: ADR-029 §6,
docs/research/alknet-call-peer-routing/findings.md§3.5 - Status: open
- Door type: Two-way
- Priority: low
- Resolution: v1 defaults to "own ops only" —
services/listshows the head's own Layer 0Externalops, filtered byAccessControl::check(calling_peer), unchanged from today (minus theremote_safefilter). Aservices/list-peersopt-in (new built-in operation) lists the peer overlays with attribution: each peer's sub-overlay listed as{ peer: Option<PeerId>, operations: [...] }, filtered by the calling peer's authorization. Whether re-exported peer ops are listed by default, opt-in, or per-peer-policy is the two-way-door remainder; v1 is opt-in (services/list-peers). The re-export policy is anAccessControldecision on the listing op. Decided during implementation when a consumer needs peer-attributed discovery; recorded here, not in a full ADR. - Cross-references: ADR-029, client-and-adapters.md
OQ-32: Multi-Hop Federation
- Origin: ADR-029 §3.7,
docs/research/alknet-call-peer-routing/findings.md§3.7 - Status: open
- Door type: One-way (federation model), two-way (mechanism)
- Priority: low
- Resolution: v1 is one-hop — worker A does not transitively see worker
B's ops through the head unless the head explicitly re-exports them. The
peer-keyed overlay model extends to multi-hop without redesign (a chain of
PeerRef::Specificrouting decisions), but path-finding (which peer reaches which op transitively) is where a graph library (petgraph) would pay off. For v1 (one hop, shallow), a nestedHashMap<PeerId, HashMap<String, ...>>suffices. Whether multi-hop federation becomes a real use case is a future decision; the peer-keyed model does not foreclose it. Not designed; tracked here so the v1 model's extendability is recorded. - Cross-references: ADR-029, client-and-adapters.md
OQ-33: PeerId — Cryptographic Identity vs Stable Logical Identifier
-
Origin: ADR-029 Assumption 1,
docs/research/alknet-call-peer-routing/findings.md§6.1 -
Status: resolved (2026-06-27 by ADR-030)
-
Door type: One-way (composition semantics), two-way (id source)
-
Priority: high
-
Resolution:
PeerIdis a logical identifier, decoupled from the cryptographic identity. It is not the raw fingerprint or API-key prefix — those change on key rotation, which would break every in-flightPeerRef::Specificand every ACL entry referencing that peer.ADR-029 established the one-way door (
PeerIdis logical, not crypto) with a v1 UUID source as a no-storage workaround. ADR-030 supersedes the UUID source:Identity.idbecomesPeerEntry.peer_id(stable across key rotation) on the fingerprint path, andPeerId = Identity.idfromIdentityProviderresolution. The UUID workaround is removed — the stable logical id is the real thing, sourced from the auth system, not an ephemeral connection-assigned value.The
PeerEntryconfig model (peer_id,fingerprint,scopes,resources,display_name,enabled) lives inAuthPolicy. Key rotation is a singlePeerEntry.fingerprintupdate — thepeer_id, ACL entries, andPeerRef::Specificreferences stay stable. The no-DB posture is preserved (core has the trait + the in-memoryConfigIdentityProvideradapter; persistence adapters are additive separate crates, ADR-033).The one-way door (preserved from ADR-029):
PeerIdis a logical id, notIdentity.id(the fingerprint). This determines thePeerCompositeEnvkey type, thePeerRef::Specificpayload type, and theScopedPeerEnv.peer_pinnedentry shape. The source of the logical id (ADR-029's UUID → ADR-030'sPeerEntry.peer_id) was the two-way-door remainder; it is now resolved. -
Cross-references: ADR-009, ADR-014, ADR-015, ADR-017, ADR-021, ADR-027, ADR-029, ADR-030, OQ-34, OQ-35, client-and-adapters.md, operation-registry.md, auth.md
OQ-34: Persistent Peer Registry (Cross-Node State Storage)
-
Origin: OQ-33 (the storage dimension it surfaced), the no-DB posture of ADR-008/018/025
-
Status: resolved (2026-06-27 by ADR-030 + ADR-031 + ADR-033)
-
Door type: One-way (storage boundary), two-way (backend choice)
-
Priority:
medium (not a v1 blocker)→ resolved -
Resolution: The storage boundary is: core defines repo traits + in-memory default adapters; persistence adapters are separate crates; the assembly layer wires the adapter. This is the repo/adapter pattern (ADR-033), already established by
IdentityProvider(ADR-004) and now extended toCredentialStore(ADR-031).IdentityProvider(ADR-004) — the auth repo trait, in core.ConfigIdentityProvideris the in-memory default, backed byAuthPolicy.peers(ADR-030). A futurealknet-peer-store-sqliteadapter that persistsPeerEntryrecords in apeerstable is additive — it implements the same trait.CredentialStore(ADR-031) — the credential repo trait, in core.InMemoryCredentialStoreis the in-memory default. A future persistence adapter is additive.
The no-DB posture of the core crates is preserved in the sense that matters: core has no backend dependency (no SQLite, no honker). The in-memory default adapters carry no persistence. The persistence adapters are additive crates, built when a concrete use case forces them, wired by the assembly layer.
The concrete adapter shapes (table schemas, backend choice, indexing, caching) are the two-way-door remainder, tracked as OQ-36 (deferred for exploration). The trait shapes are the one-way door, committed by ADR-030, ADR-031, and ADR-033.
-
Cross-references: ADR-008, ADR-018, ADR-021, ADR-025, ADR-029, ADR-030, ADR-031, ADR-033, OQ-33, OQ-36, auth.md, config.md
Theme: Storage and Adapters
OQ-35: API Key Identity vs Peer Identity
-
Origin: ADR-030 §"API keys" (the asymmetry between the two auth paths)
-
Status: resolved (recorded by ADR-030, not a blocker)
-
Door type: One-way (the asymmetry is deliberate, not an oversight)
-
Priority: medium
-
Resolution: The fingerprint auth path gets the
PeerEntryid-decoupling treatment (Identity.id = peer_id, stable across key rotation); the API-key auth path does not (Identity.id = prefix, changes with the key). This is deliberate:- Node identity (fingerprint path) must survive key rotation — the
same logical node rotates its TLS key, and every ACL entry / routing
reference to it should stay stable.
PeerEntryprovides this. - Bearer-token identity (API-key path) IS the token — rotating the key means a new prefix and a new identity, by design (revocation is the rotation mechanism for API keys). Decoupling the API key identity from the prefix would solve a problem API keys don't have.
The asymmetry is documented in
auth.md("API keys vs peer entries") and in ADR-030 §"API keys" so it's explicit, not an oversight. See auth.md for the table comparing the two paths. - Node identity (fingerprint path) must survive key rotation — the
same logical node rotates its TLS key, and every ACL entry / routing
reference to it should stay stable.
OQ-36: Concrete Adapter Shapes (Deferred for Exploration)
-
Origin: ADR-033 §"What this does NOT do" (concrete adapter shapes not specified), the project's note that the repo pattern is a tool to reach for, not a one-size-fits-all mold
-
Status: open (deferred for exploration)
-
Door type: Two-way (adapter shapes are implementation details; the trait shapes are the one-way doors, already committed by ADR-030/031/033)
-
Priority: low (becomes real when a persistence use case forces a concrete adapter build)
-
Resolution: The repo/adapter pattern is committed (ADR-033): core defines repo traits + in-memory default adapters; persistence adapters are separate crates; the assembly layer wires the adapter. The concrete adapter shapes — table schemas, backend choice (SQLite + honker vs. a key-value store vs. a remote service), indexing, caching, connection management — are deferred for exploration.
The project is iterating on adapter simplification. The trait shapes (
IdentityProvider,CredentialStore) are the commitment; the adapter shapes are not. When a concrete use case (peer identity persistence across restarts, credential persistence across restarts, ACL delegation graph) forces a persistence adapter build, the adapter shape gets reasoned through then, not speculatively now.This OQ exists so the deferral is deliberate, not accidental — the pattern is committed, the adapters are not, and the gap is tracked.
-
Cross-references: ADR-030, ADR-031, ADR-033, OQ-34, auth.md, config.md