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.
This commit is contained in:
2026-06-23 10:56:05 +00:00
parent cb98f42cd4
commit 2e34590522
14 changed files with 1129 additions and 120 deletions

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-06-22-16
last_updated: 2026-06-23
---
# Core Types
@@ -178,6 +178,67 @@ in `StreamError`, but once a handler propagates via `HandlerError`, the
endpoint treats all variants as "close the connection" (one-ALPN-per-
connection, ADR-006).
## Capabilities
Outbound credentials injected by the assembly layer at registration time.
A handler uses `Capabilities` to make authenticated outbound calls (LLM
provider API keys, HTTP service tokens, signing keys). See ADR-014 for the
secret-material flow and ADR-022 for the registration-bundle wiring.
```rust
/// Outbound credentials for a handler. Non-serializable, zeroized,
/// immutable after construction. `Clone` is required by the composition
/// model (`parent.capabilities.clone()` in `OperationEnv::invoke()`).
///
/// The concrete internal shape (a typed map, a struct with named fields)
/// is a two-way door, but the public API is fixed: `new()`, `with_api_key()`,
/// `with_http_token()`, and `get()`. Fields are private — callers cannot
/// mutate the credentials after construction. This makes the clone-semantics
/// two-way door genuinely two-way: Arc-based clone (shared immutable state)
/// and deep-copy clone (isolated state) are behaviorally identical when
/// neither supports mutation. See ADR-014, ADR-022, review #002 W2.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct Capabilities {
// Private — no interior mutability. The builder API (new, with_*) is
// the only construction path. Immutability after construction is the
// security guard that makes clone semantics safe.
entries: HashMap<String, Secret<String>>,
}
impl Capabilities {
/// Empty capabilities — for handlers that make no outbound calls.
pub fn new() -> Self;
/// Add an API key (e.g., "google", "openai") to the capabilities.
pub fn with_api_key(mut self, service: &str, key: String) -> Self;
/// Add an HTTP bearer token (e.g., "vastai", "github") to the capabilities.
pub fn with_http_token(mut self, service: &str, token: String) -> Self;
/// Retrieve a credential by service name, if present.
pub fn get(&self, service: &str) -> Option<&Secret<String>>;
}
```
- **Non-serializable**: `Capabilities` does **not** derive `Serialize`. It
cannot appear in `EventEnvelope` payloads even by accident. This is a
type-level enforcement of ADR-014's "call protocol carries no secret material."
- **Zeroized**: derives `Zeroize` and `ZeroizeOnDrop`. Secret material does
not linger in freed heap memory.
- **`Clone` + `Send + Sync`**: required by the composition model —
`OperationEnv::invoke()` clones the parent's capabilities for each child.
`Send + Sync` is required because the context is held across async task
boundaries.
- **Immutable after construction**: no `set`, no `insert`, no `mut` accessors.
This is the guard from review #002 W2 — it makes the Arc-vs-deep-copy clone
semantics genuinely two-way (shared immutable state is safe).
- **Module location**: `Capabilities` lives in alknet-core (it's a shared type
— see overview.md's Shared Types table). alknet-call imports it.
See [operation-registry.md → Capability Injection](../call/operation-registry.md#capability-injection)
for how the dispatch path populates `OperationContext.capabilities` from the
registration bundle.
## Design Decisions
| Decision | ADR | Summary |
@@ -187,6 +248,7 @@ connection, ADR-006).
| HandlerError is non-fatal | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Handler errors close the connection, not the endpoint |
| SendStream/RecvStream wrap quinn + iroh | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Internal enum dispatch for both QUIC sources |
| Connection stores handler-resolved identity | OQ-11 (resolved) | `set_identity` via `OnceLock` — write-once-read-many; read by handler-side logging, not by the endpoint (C13 resolved) |
| Capabilities type | [ADR-014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Non-serializable, zeroized, immutable after construction; `Clone` for composition propagation |
## Open Questions