Files
alknet/tasks/core/identity-store-trait.md
glm-5.2 df355c53a9 tasks: decompose ADR-029/030/031/032/034/035 source sync into 17 tasks
Decompose the source-to-spec sync for the core and call crates into atomic,
dependency-ordered tasks for implementation agents:

Core (7 tasks + review):
- peer-entry-model: PeerEntry struct, AuthPolicy.peers (ADR-030 keystone)
- credential-store-trait: CredentialStore/InMemoryCredentialStore/StoreError (ADR-031/035)
- identity-store-trait: IdentityStore async write trait (ADR-035)
- config-identity-provider-peerentry: ConfigIdentityProvider PeerEntry resolution (ADR-030)
- fingerprint-normalization: ed25519:hex for raw keys across quinn/iroh (ADR-030 §6)
- three-remote-roles-docs: document ADR-034 roles and verifier selection
- review-core-sync: phase gate before call consumes new identity semantics

Call (9 tasks + review):
- retire-remote-safe: remove ADR-028 machinery, AccessControl is the gate (ADR-029 §3)
- operation-context-forwarded-for: forwarded_for field, wire-ingress only (ADR-032)
- peer-composite-env: PeerCompositeEnv, PeerId=Identity.id, remove UUID (ADR-029/030)
- operation-env-invoke-peer: invoke_peer/peer_contains/PeerRef (ADR-029 §2)
- services-list-accesscontrol-filtered: AccessControl filter, list-peers opt-in (ADR-029 §6)
- call-client-verifier-selection: TLS client-auth, verifier by PeerEntry (OQ-29, ADR-034)
- from-call-forwarded-for: populate forwarded_for, peer-keyed registration (ADR-029 §5, ADR-032)
- dispatch-peer-identity: AccessControl::check(peer_identity), PeerId from resolution (ADR-029 §3, ADR-030 §5)
- review-call-sync: phase gate for the call sync

Validated: 58 tasks, no cycles, logical topo order, two review checkpoints.
2026-06-28 21:08:41 +00:00

4.4 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) pending
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

To be filled on completion