Files
alknet/docs/architecture/decisions/018-vault-standalone-crate.md
glm-5.2 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).
2026-06-22 14:53:52 +00:00

7.7 KiB

ADR-018: Vault as Standalone Crate

Status

Accepted

Context

alknet-vault provides BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, BIP-0032 secp256k1 derivation (feature-gated), and AES-256-GCM encryption. It holds the master seed — the root of trust for all derived keys and encrypted credentials in the alknet system.

The question is: what does alknet-vault depend on? The candidates:

  1. Depend on alknet-core for shared types (errors, maybe Identity). This pulls QUIC, quinn, iroh, rustls, and tokio runtime dependencies into the vault's dependency tree.
  2. Stand alone — zero alknet crate dependencies. The vault defines its own types, its own error enum. Other crates depend on the vault; the vault depends on nothing in alknet.

This is a one-way door. Once the vault depends on alknet-core, reversing it requires removing that dependency from every type, error conversion, and test — and the longer it stays, the more entangled it becomes.

Why standalone matters

The vault is used in contexts where QUIC networking does not exist:

  • CLI tools: a key-derivation utility that derives an identity key from a mnemonic without starting a network endpoint.
  • Test harnesses: integration tests in other crates derive test keys without spinning up a QUIC endpoint.
  • WASM key derivation: a future WASM target that derives keys in a browser (the BiStream trait in ADR-007 preserves this door at the transport layer; the vault's independence preserves it at the secret layer).
  • Embedded assembly: a binary that only needs the vault to decrypt a config file at startup, with no networking at all.

If the vault depends on alknet-core, all of these contexts pull in quinn, iroh, rustls, and tokio — none of which they need. The vault's job is cryptographic derivation and encryption. It has no networking concern.

What the vault provides without alknet-core

The vault defines its own types and traits:

  • Mnemonic, Seed — BIP39 root material
  • ExtendedPrivKey (Ed25519), Secp256k1ExtendedPrivKey (Ethereum) — derived key material
  • DerivedKey, KeyType — protocol-level key representation
  • EncryptedData, EncryptionKey — AES-256-GCM blobs
  • VaultServiceHandle — runtime API (direct method calls; no actor, no message enum — see ADR-025)
  • VaultServiceError — its own error enum (string-wrapped sub-errors; the vault doesn't share an error type with alknet-core)

The vault uses direct method calls on VaultServiceHandle, not irpc dispatch (ADR-025). The vault is local-only by construction — no remote dispatch capability, no RemoteService trait, no wire format for vault messages. If remote vault access is ever needed, it's a separate crate that wraps the vault (see ADR-025, OQ-021).

Decision

alknet-vault has zero alknet crate dependencies. It depends only on external crates (bip39, ed25519-bip32, aes-gcm, sha2, hmac, secp256k1, tokio for RwLock sync primitives, serde, zeroize, thiserror, base64, rand). ADR-025 dropped irpc, irpc-derive, and postcard — the vault no longer uses irpc dispatch.

The vault does not depend on:

  • alknet-core — no shared types, no Identity, no AuthContext
  • alknet-call — no OperationSpec, no OperationContext, no call protocol
  • alknet-vault does not implement ProtocolHandler — it has no ALPN (see ADR-019)

Dependency flow is strictly one-directional:

alknet-vault (standalone)
      ↑
alknet (CLI binary) — the only crate that depends on alknet-vault

No handler crate depends on alknet-vault directly. Handlers receive derived material through capabilities injected by the assembly layer (ADR-014). The CLI binary is the sole integration point (ADR-008, ADR-019).

Type independence

The vault defines its own types and does not share types with alknet-core:

  • VaultServiceError is the vault's error enum. It is a plain thiserror::Error (ADR-025 dropped irpc, so vault errors no longer need Serialize/Deserialize for wire dispatch). It does not implement From for alknet-core error types — the CLI binary converts at the assembly boundary.
  • DerivedKey is the vault's key representation. It is not shared with alknet-core's Identity type. The CLI binary extracts the bytes it needs (private key for signing, public key for TLS identity) and constructs the alknet-core types at the assembly layer.
  • EncryptedData is the vault's encrypted blob format. It is shared with alknet-storage (a future crate) by type-level agreement, not by a crate dependency — both crates must agree on the serialization format (see encryption.md).

Consequences

Positive:

  • The vault compiles and runs without QUIC, quinn, iroh, rustls, or a tokio runtime (the VaultServiceHandle works with just std::sync::RwLock; the actor uses tokio::sync::mpsc but that's a lightweight dependency).
  • CLI tools, test harnesses, and future WASM targets can use the vault for key derivation without pulling in networking crates.
  • The vault's API surface is stable — changes to alknet-core types don't force a vault recompile, and changes to vault types don't force a handler recompile (the CLI is the only consumer).
  • No circular dependency risk. The dependency graph is a strict DAG.
  • The vault can be published and used independently of alknet — it's a general-purpose local key vault, not an alknet-specific component.

Negative:

  • The vault cannot share types with alknet-core. If a type wants to be shared (e.g., a future Fingerprint type), it must live in alknet-core and the vault must define its own equivalent, or a new shared crate must be created. This is a feature, not a bug — it forces explicit boundaries.
  • The CLI binary must convert between vault types and alknet-core types at the assembly boundary. This is a small amount of glue code (extract bytes from DerivedKey, construct alknet-core types). See ADR-019.
  • The vault's VaultServiceError is separate from alknet-core's HandlerError. The CLI binary maps vault errors to handler errors or startup failures. This is expected — the vault is a library, not a handler.

Assumptions

  1. The vault's API is consumed by one component (the CLI binary) in the alknet system. If a future use case requires multiple crates to depend on the vault directly, the dependency flow still holds — they depend on the vault, the vault depends on nothing. The standalone property is preserved.

  2. Shared types between the vault and other crates are agreed by type-level compatibility, not by a crate dependency. EncryptedData is the example: both the vault and alknet-storage (future) must agree on the serialization format. This is documented in the type's spec, not enforced by the type system across crates.

  3. The vault's error type does not need to integrate with alknet-core's error handling. The vault returns VaultServiceError; the CLI binary handles it at the assembly boundary. If a future use case requires propagating vault errors through alknet-core's error types, the CLI converts at the boundary.

References

  • ADR-003: Crate decomposition (alknet-vault is standalone)
  • ADR-005: irpc as call protocol foundation (irpc remains the foundation for alknet-call; the vault no longer uses irpc — see ADR-025)
  • ADR-025: Vault local-only dispatch (dropped irpc from the vault; the vault uses direct method calls, no actor, no remote capability)
  • ADR-008: Vault integration point (CLI-embedded, assembly-layer only)
  • ADR-014: Secret material flow and capability injection
  • ADR-019: Vault assembly-layer-only access
  • crates/vault/README.md
  • Implementation: crates/alknet-vault/