Files
alknet/tasks/core/identity-store-trait.md

4.9 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
core/identity-store-trait Add IdentityStore async write trait extending IdentityProvider (ADR-035) completed
core/peer-entry-model
single low component implementation

Description

Add the IdentityStore async write trait for peer management, extending the read-only IdentityProvider trait. Per ADR-035 §2.

IdentityProvider is read-only today and stays read-only — it is the hot-path trait called on every incoming connection (sync, no .await). Peer mutations (add/update/remove a PeerEntry) go through this separate async trait.

IdentityStore trait

/// Write trait — management path, async (ADR-035). ConfigIdentityProvider
/// does NOT implement this (config reload is its write path — see below).
/// SqliteIdentityProvider does: writes hit SQLite, emit honker NOTIFY,
/// and the local LISTEN refreshes the in-memory read index.
#[async_trait]
pub trait IdentityStore: IdentityProvider {
    async fn put_peer(&self, peer: &PeerEntry) -> Result<(), StoreError>;
    async fn update_peer(&self, peer_id: &str, peer: &PeerEntry) -> Result<(), StoreError>;
    async fn remove_peer(&self, peer_id: &str) -> Result<(), StoreError>;
}
  • put_peer — insert or replace a PeerEntry (upsert by peer_id).
  • update_peer — update an existing PeerEntry (error if peer_id not found; for upsert semantics use put_peer).
  • remove_peer — delete a PeerEntry by peer_id.

Why a separate trait, not async methods on IdentityProvider

  • The hot-path read trait is consumed by the accept loop and every handler — those call sites are sync and must not gain .await. If put_peer were on IdentityProvider, every consumer would see the async method even though only the management path calls it. A separate IdentityStore: IdentityProvider supertrait keeps the read surface lean and makes the write surface opt-in.
  • ConfigIdentityProvider does not implement IdentityStore. Its write path is config reload (ConfigReloadHandle::reload), not a method call. This preserves the config-is-source-of-truth model. Implementing IdentityStore

for ConfigIdentityProvider "for symmetry" would violate that model — the constraint is the absence of a backend, not a type-system constraint.

ConfigIdentityProvider posture

ConfigIdentityProvider deliberately does NOT implement IdentityStore. This task does not change ConfigIdentityProvider — it only adds the trait. The trait is defined for future adapters (SqliteIdentityProvider in alknet-store-sqlite) to implement. StoreError is already defined by core/credential-store-trait.

Module placement

Add IdentityStore alongside IdentityProvider in alknet-core/src/auth.rs (or a new store module if CredentialStore landed there). Re-export from lib.rs.

Acceptance Criteria

  • IdentityStore trait with put_peer, update_peer, remove_peer (all async)
  • IdentityStore: IdentityProvider (supertrait)
  • StoreError used as the error type (from core/credential-store-trait)
  • ConfigIdentityProvider does NOT implement IdentityStore
  • #[async_trait] on the trait
  • No changes to IdentityProvider trait (stays read-only, sync)
  • Unit test: a mock/test impl of IdentityStore compiles and works (verify the trait is implementable)
  • Unit test: ConfigIdentityProvider does not implement IdentityStore (compile-time or trait-bound assertion)
  • cargo test -p alknet-core succeeds
  • cargo clippy -p alknet-core succeeds with no warnings

References

  • docs/architecture/crates/core/auth.md — IdentityStore write trait, ConfigIdentityProvider posture
  • docs/architecture/decisions/035-concrete-persistence-adapter-shapes.md — ADR-035 §2 (the trait, read/write split rationale)
  • docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md — ADR-033 (the pattern)

Notes

Small task but locks the trait shape — a one-way door. The read/write split keeps the hot path sync (no .await in the accept loop). ConfigIdentityProvider not implementing IdentityStore is a design posture, not a type-system constraint: it holds no backend, and its write path is config reload. A deployment that wants method-call peer management wires the SQLite adapter (a separate crate, not built in this sync).

Summary

Added the IdentityStore async write trait (put_peer/update_peer/remove_peer) extending the read-only IdentityProvider supertrait in crates/alknet-core/src/auth.rs, re-exported from lib.rs. Uses StoreError from core/credential-store-trait. ConfigIdentityProvider deliberately does NOT implement IdentityStore (verified by trait-bound assertion test). Added a MockIdentityStore test impl covering upsert, update-existing, update-missing→NotFound, remove-existing, remove-missing→NotFound, and IdentityProvider conformance. 126 tests pass, cargo build/clippy clean.