docs(research): rewrite storage/auth strategy — concrete repo/adapter design, no deferrals
Reworks the storage strategy doc to commit to concrete design, replacing
the 'when storage arrives' / 'future' / 'later' framing that was putting off
important work.
Key changes from the previous draft:
- §4 (Repo/Adapter Pattern): now an explicit design with the trait contracts
(IdentityProvider, CredentialStore), the adapter contracts
(ConfigIdentityProvider with PeerEntry update, SqliteIdentityProvider,
InMemoryCredentialStore, SqliteCredentialStore), and the concrete table
schemas. Not a pattern description — a design commitment.
- §4: PeerEntry config model — AuthPolicy gains peers: Vec<PeerEntry>
replacing authorized_fingerprints: HashSet<String>. This is the
id-fingerprint decoupling (OQ-33) done as a config change, not a storage
change. ConfigIdentityProvider resolves fingerprint → PeerEntry →
Identity { id: peer_id } (stable, not the fingerprint).
- §7 (Decomposition): the 'what goes where' table now has a Status column
(exists / needs adding / needs building / needs PeerEntry update) instead
of 'future'. The crate graph is a concrete build plan.
- §10 (Build Order): replaces 'What This Means for the Immediate Path' (which
had 'when storage arrives' framing) with a 4-tier dependency-driven build
order. Tier 1 = core repo traits + PeerEntry config model. Tier 2 = SQLite
adapters. Tier 3 = ADR-029 migration + forwarded_for. Tier 4 = alknet-graphs
(built when a graph-shaped problem exists, not speculatively).
- §10: explicit 'What does NOT get built (dropped, not deferred)' section —
multi-tenant, accounts/orgs, secrets module, single storage crate are
dropped, not deferred.
- All 'future' / 'when X arrives' / 'v1' / 'phase n' language removed for
things that are needed. The only 'when X is needed' language remaining is
for genuinely non-existent problems (ACL delegation, workflows, taskgraph)
— those are built when the problem exists, not speculatively.
This commit is contained in:
@@ -7,16 +7,14 @@ last_updated: 2026-06-27
|
|||||||
|
|
||||||
**Status**: Draft for iteration
|
**Status**: Draft for iteration
|
||||||
**Date**: 2026-06-27
|
**Date**: 2026-06-27
|
||||||
**Scope**: Cross-cutting — storage decomposition, auth/ACL model, repo pattern,
|
**Scope**: Cross-cutting — storage decomposition, auth/ACL model, repo/adapter
|
||||||
SQLite+honker as foundation, metagraph as tool. Synthesizes the discussion
|
pattern, SQLite+honker as foundation, metagraph as tool. Synthesizes the
|
||||||
that surfaced during the peer-graph routing research (ADR-029) and OQ-33/34
|
discussion that surfaced during the peer-graph routing research (ADR-029) and
|
||||||
resolution.
|
OQ-33/34 resolution.
|
||||||
|
|
||||||
This document consolidates a multi-thread discussion into an architectural
|
This document consolidates a multi-thread discussion into an architectural
|
||||||
strategy for storage and auth in the alknet crate graph. It is not an ADR —
|
strategy for storage and auth in the alknet crate graph. It is not an ADR —
|
||||||
it's the research that will inform ADRs and spec amendments. The
|
it's the research that will inform ADRs and spec amendments.
|
||||||
implementation-relevant pieces (the `forwarded_for` field, the
|
|
||||||
`IdentityProvider`-as-repo framing) get folded into specs after review.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,8 +26,8 @@ for it?
|
|||||||
|
|
||||||
1. **Peer identity (OQ-33/OQ-34)** — a head node needs to persist the mapping
|
1. **Peer identity (OQ-33/OQ-34)** — a head node needs to persist the mapping
|
||||||
from a stable logical peer identity to its current cryptographic material,
|
from a stable logical peer identity to its current cryptographic material,
|
||||||
surviving key rotation and restarts. The UUID workaround is ephemeral; the
|
surviving key rotation and restarts. The UUID workaround is ephemeral; a
|
||||||
real solution is a store.
|
real store is needed.
|
||||||
2. **Filesystem (POC-validated)** — SQLite + honker + iroh-blobs as the
|
2. **Filesystem (POC-validated)** — SQLite + honker + iroh-blobs as the
|
||||||
three-layer stack for path-tree metadata, content-addressed blobs, and
|
three-layer stack for path-tree metadata, content-addressed blobs, and
|
||||||
transactional notify-on-commit. 24 tests across two POC crates.
|
transactional notify-on-commit. 24 tests across two POC crates.
|
||||||
@@ -45,6 +43,11 @@ data. The question is how to decompose this so the core crates stay lean
|
|||||||
while the storage-dependent crates get what they need — without forcing
|
while the storage-dependent crates get what they need — without forcing
|
||||||
everything through the same abstraction.
|
everything through the same abstraction.
|
||||||
|
|
||||||
|
The answer is a **repo/adapter pattern**: core defines traits, adapters
|
||||||
|
implement them against specific backends, the assembly layer wires the
|
||||||
|
adapter. This is not a deferral — the traits and the adapters are concrete
|
||||||
|
design commitments, documented below.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. The Principle: Right Tool for the Right Shape
|
## 2. The Principle: Right Tool for the Right Shape
|
||||||
@@ -129,20 +132,28 @@ wakes on commit, not on poll.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. The Repo Pattern for Auth
|
## 4. The Repo/Adapter Pattern
|
||||||
|
|
||||||
### The existing pattern (make it explicit)
|
### The principle
|
||||||
|
|
||||||
`alknet-core` already has the repo pattern: `IdentityProvider` is a trait
|
Core defines traits (repo interfaces). Adapters implement them against
|
||||||
with two methods (`resolve_from_fingerprint`, `resolve_from_token`), one
|
specific backends. The assembly layer wires the adapter. Downstream crates
|
||||||
adapter (`ConfigIdentityProvider`, backed by `ArcSwap<DynamicConfig>`), and
|
consume the trait, not the adapter. This is the same pattern `IdentityProvider`
|
||||||
one consumer (the call protocol's `Dispatcher`). This is a repo trait — it
|
already establishes — we're making it explicit and extending it to every
|
||||||
abstracts the *what* (resolve an identity from a credential) from the *how*
|
storage-shaped concern.
|
||||||
(in-memory config, SQLite, Redis, remote service).
|
|
||||||
|
|
||||||
**Make this explicit.** `IdentityProvider` is the auth repo trait in core.
|
### Reference: kepal
|
||||||
Adapters implement it. The assembly layer wires the adapter. Downstream
|
|
||||||
crates consume the trait, not the adapter.
|
The TypeScript project [kepal](/workspace/keypal) is a clean example. It
|
||||||
|
abstracts API key management (hashing, validation, scopes, expiration,
|
||||||
|
caching) with a `Storage` interface and adapters for Redis, Drizzle, Prisma,
|
||||||
|
Kysely, Convex, and in-memory. The core logic (`Manager`) is backend-agnostic;
|
||||||
|
the storage is a trait; the consumer picks the adapter at wiring time. An
|
||||||
|
`AdapterFactory` provides column-mapping / schema-config so the same adapter
|
||||||
|
works against different table schemas.
|
||||||
|
|
||||||
|
The alknet equivalent: core defines the repo trait, adapters implement it,
|
||||||
|
the assembly layer wires the adapter. The shapes map cleanly.
|
||||||
|
|
||||||
### Why this matters beyond the call crate
|
### Why this matters beyond the call crate
|
||||||
|
|
||||||
@@ -153,44 +164,141 @@ a repo trait in core, those crates use the same trait, the same adapters, and
|
|||||||
potentially the same backing store — without depending on alknet-call. The
|
potentially the same backing store — without depending on alknet-call. The
|
||||||
call crate is one consumer of auth, not the owner of it.
|
call crate is one consumer of auth, not the owner of it.
|
||||||
|
|
||||||
### The distributed-auth door
|
The repo pattern also opens the door to distributed auth adapters (automerge
|
||||||
|
sync, Redis, a remote identity service) — the trait doesn't care which
|
||||||
|
backend is wired. That's not designed here, but the pattern doesn't foreclose
|
||||||
|
it.
|
||||||
|
|
||||||
If the repo trait is clean, someone can wire an adapter that syncs via
|
### The concrete repo traits and adapters
|
||||||
automerge (like the filesystem POC's path-tree CRDT), a Redis adapter, or a
|
|
||||||
remote-service adapter. The trait doesn't care. Auth data that isn't storing
|
|
||||||
sensitive details (unless encrypted) could be distributed via the same
|
|
||||||
patterns the filesystem uses for its path tree. This isn't designed here —
|
|
||||||
it's a door the repo pattern opens by not foreclosing it.
|
|
||||||
|
|
||||||
### Reference: kepal
|
This is the design commitment, not a deferral:
|
||||||
|
|
||||||
The TypeScript project [kepal](/workspace/keypal) is a clean example of this
|
#### `IdentityProvider` (auth repo trait — already in core)
|
||||||
pattern. It abstracts API key management (hashing, validation, scopes,
|
|
||||||
expiration, caching) with a `Storage` interface and adapters for Redis,
|
|
||||||
Drizzle, Prisma, Kysely, Convex, and in-memory. The core logic
|
|
||||||
(`Manager`) is backend-agnostic; the storage is a trait; the consumer picks
|
|
||||||
the adapter at wiring time. An `AdapterFactory` provides column-mapping /
|
|
||||||
schema-config so the same adapter works against different table schemas.
|
|
||||||
|
|
||||||
The alknet equivalent: `IdentityProvider` is the trait (like kepal's
|
```rust
|
||||||
`Storage`), `ConfigIdentityProvider` is the in-memory adapter (like kepal's
|
pub trait IdentityProvider: Send + Sync + 'static {
|
||||||
`MemoryStore`), the SQLite peer registry is the real adapter (like kepal's
|
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity>;
|
||||||
`RedisStore`/`DrizzleStore`), and the assembly layer wires the adapter (like
|
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity>;
|
||||||
kepal's `Manager` constructor). The shapes map cleanly.
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### PeerStore: adapter-internal, not core
|
Already exists. Already used by the call protocol's `Dispatcher`. The
|
||||||
|
contract is: given a credential (fingerprint or token), return the resolved
|
||||||
|
`Identity` (id, scopes, resources). The `Identity.id` is the **stable logical
|
||||||
|
peer identity**, decoupled from the fingerprint (OQ-33). The adapter maps
|
||||||
|
fingerprint → stable id + scopes + resources.
|
||||||
|
|
||||||
|
**Adapters that need to exist:**
|
||||||
|
|
||||||
|
1. **`ConfigIdentityProvider`** (exists, needs updating) — backed by
|
||||||
|
`ArcSwap<DynamicConfig>`. Today it sets `Identity.id = fingerprint`, which
|
||||||
|
couples the identity to the crypto material and breaks on key rotation.
|
||||||
|
Needs to be updated to use `PeerEntry` (see below) so `Identity.id` is the
|
||||||
|
stable `peer_id`, not the fingerprint.
|
||||||
|
|
||||||
|
2. **`SqliteIdentityProvider`** (needs building) — backed by a `peers` table
|
||||||
|
in SQLite + honker. Implements `IdentityProvider` by querying the `peers`
|
||||||
|
table. This is the persistent adapter that survives restarts and supports
|
||||||
|
runtime peer add/remove/update. The `peers` table is:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE peers (
|
||||||
|
peer_id TEXT PRIMARY KEY, -- stable logical id ("worker-a")
|
||||||
|
fingerprint TEXT NOT NULL, -- current crypto material
|
||||||
|
scopes TEXT NOT NULL DEFAULT '[]', -- JSON array
|
||||||
|
resources TEXT NOT NULL DEFAULT '{}', -- JSON map
|
||||||
|
display_name TEXT,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_peers_fingerprint ON peers(fingerprint);
|
||||||
|
```
|
||||||
|
|
||||||
|
Key rotation: `UPDATE peers SET fingerprint = ?new WHERE peer_id = ?`. The
|
||||||
|
`peer_id` is stable; ACL entries key on it; the fingerprint changes; the
|
||||||
|
ACL still matches.
|
||||||
|
|
||||||
|
3. **In-memory `IdentityProvider`** (exists for tests) — the current
|
||||||
|
`ConfigIdentityProvider` with `AuthPolicy::default()` or a test config.
|
||||||
|
|
||||||
|
#### `CredentialStore` (encrypted credentials repo trait — needs adding to core)
|
||||||
|
|
||||||
|
The http crate's `from_openapi`/`from_mcp` handlers need provider credentials
|
||||||
|
(API keys, OAuth tokens). The vault encrypts them; a store persists the
|
||||||
|
encrypted blobs. The trait:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait CredentialStore: Send + Sync {
|
||||||
|
fn get(&self, provider: &str) -> Option<EncryptedData>;
|
||||||
|
fn put(&self, provider: &str, data: &EncryptedData) -> Result<(), CredentialStoreError>;
|
||||||
|
fn delete(&self, provider: &str) -> Result<(), CredentialStoreError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Adapters:**
|
||||||
|
1. **`InMemoryCredentialStore`** — `HashMap<String, EncryptedData>`. For
|
||||||
|
tests and simple deployments where credentials are loaded from config at
|
||||||
|
startup.
|
||||||
|
2. **`SqliteCredentialStore`** — `credentials` table in SQLite + honker.
|
||||||
|
Persists encrypted provider credentials. The vault encrypts; the store
|
||||||
|
persists the `EncryptedData` blob; the assembly layer loads them into
|
||||||
|
`Capabilities` at registration time (the no-env-vars invariant, ADR-014).
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE credentials (
|
||||||
|
provider TEXT PRIMARY KEY, -- "openai", "anthropic", etc.
|
||||||
|
encrypted_data TEXT NOT NULL, -- EncryptedData JSON (key_version, iv, ciphertext)
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `PeerStore` (adapter-internal, not a core trait)
|
||||||
|
|
||||||
A `PeerStore` trait (save/find/update/delete peer records) is an
|
A `PeerStore` trait (save/find/update/delete peer records) is an
|
||||||
*adapter-internal* detail, not a core trait. The core trait is
|
*adapter-internal* detail, not a core trait. The core trait is
|
||||||
`IdentityProvider`. The SQLite adapter implements `IdentityProvider` by
|
`IdentityProvider`. The `SqliteIdentityProvider` implements
|
||||||
delegating to a `PeerStore` internally. The trait boundary that matters for
|
`IdentityProvider` by delegating to an internal `PeerStore` (which queries
|
||||||
cross-crate sharing is `IdentityProvider`, not `PeerStore`.
|
the `peers` table). The `ConfigIdentityProvider` implements
|
||||||
|
`IdentityProvider` by reading `PeerEntry` from config. The trait boundary
|
||||||
|
that matters for cross-crate sharing is `IdentityProvider`, not `PeerStore`.
|
||||||
|
|
||||||
This keeps core lean: one auth trait (`IdentityProvider`), not two. The
|
This keeps core lean: the auth repo trait (`IdentityProvider`) and the
|
||||||
store trait lives in the adapter crate (or the assembly layer), where it's
|
credential repo trait (`CredentialStore`) are in core. The store traits
|
||||||
an implementation detail. If a future adapter (Redis, remote service) needs
|
(`PeerStore`, etc.) are adapter-internal.
|
||||||
a different internal store shape, it's free to define one — the core contract
|
|
||||||
is `IdentityProvider`, not the store.
|
### The `PeerEntry` config model
|
||||||
|
|
||||||
|
`AuthPolicy` needs to support the id-fingerprint decoupling. Today it has
|
||||||
|
`authorized_fingerprints: HashSet<String>` — just fingerprints, no stable id.
|
||||||
|
The update:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct PeerEntry {
|
||||||
|
pub peer_id: String, // stable logical id ("worker-a")
|
||||||
|
pub fingerprint: String, // current crypto material
|
||||||
|
pub scopes: Vec<String>,
|
||||||
|
pub resources: HashMap<String, Vec<String>>,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthPolicy {
|
||||||
|
pub peers: Vec<PeerEntry>, // replaces authorized_fingerprints
|
||||||
|
pub api_keys: Vec<ApiKeyEntry>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ConfigIdentityProvider::resolve_from_fingerprint` queries `peers` for the
|
||||||
|
matching fingerprint and returns `Identity { id: peer.peer_id, scopes:
|
||||||
|
peer.scopes, resources: peer.resources }`. The `Identity.id` is the stable
|
||||||
|
`peer_id`, not the fingerprint. Key rotation: update the `fingerprint` field
|
||||||
|
in the `PeerEntry`; the `peer_id` and all ACL entries stay stable.
|
||||||
|
|
||||||
|
This is a config change to `AuthPolicy`, not a storage change. It works
|
||||||
|
in-memory from config, without SQLite. The SQLite adapter (`SqliteIdentityProvider`)
|
||||||
|
stores the same `PeerEntry` shape in a table and persists across restarts.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -255,22 +363,13 @@ hub's end users. The hub's ACL handles end-user authorization.
|
|||||||
|
|
||||||
### No global ACL, no replication
|
### No global ACL, no replication
|
||||||
|
|
||||||
Each node's ACL is local — in its own SQLite file (when storage arrives), in
|
Each node's ACL is local — in its own SQLite file (when the SQLite adapter
|
||||||
its own `peers` table, checked by its own `AccessControl`. There is no
|
is wired), in its own `peers` table, checked by its own `AccessControl`.
|
||||||
global ACL, no cross-service ACL replication. When a user's key rotates, the
|
There is no global ACL, no cross-service ACL replication. When a user's key
|
||||||
hub's `peers` table updates her fingerprint. The spoke's `peers` table is
|
rotates, the hub's `peers` table updates her fingerprint. The spoke's `peers`
|
||||||
unchanged — it only knows about the hub. When the hub's key rotates, the
|
table is unchanged — it only knows about the hub. When the hub's key
|
||||||
spoke's `peers` table updates the hub's fingerprint — a single entry update,
|
rotates, the spoke's `peers` table updates the hub's fingerprint — a single
|
||||||
not a full ACL replication.
|
entry update, not a full ACL replication.
|
||||||
|
|
||||||
### The "many DBs" concern
|
|
||||||
|
|
||||||
Having many SQLite files (one per node, one per concern) looks like the
|
|
||||||
microservices ACL-replication mess. It isn't, because the trust model is
|
|
||||||
per-node: each node only authorizes its direct callers. The DBs don't
|
|
||||||
overlap. The mess only happens if you try end-to-end identity propagation
|
|
||||||
(the spoke needs to know about every end user) — that's the anti-pattern,
|
|
||||||
and the repo pattern + per-node ACL avoids it.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -292,10 +391,11 @@ quotas, or pass it to the operation handler for context. But the spoke's ACL
|
|||||||
still authorizes the *hub*, not the end user — the forwarded-for identity is
|
still authorizes the *hub*, not the end user — the forwarded-for identity is
|
||||||
informational, not authoritative.
|
informational, not authoritative.
|
||||||
|
|
||||||
### The recommendation: add it, as metadata
|
### The decision: add it, as metadata
|
||||||
|
|
||||||
The forwarded-for identity should be added as a protocol-level field, not
|
The forwarded-for identity is a protocol-level field. It's either in the
|
||||||
as an afterthought. Reasoning:
|
model or it isn't — it can't be bolted on without a protocol change. The
|
||||||
|
recommendation is to include it:
|
||||||
|
|
||||||
1. **Audit trail.** Without it, a cross-node call chain is untraceable at
|
1. **Audit trail.** Without it, a cross-node call chain is untraceable at
|
||||||
the leaf. The spoke knows "the hub called me" but not "alice asked the
|
the leaf. The spoke knows "the hub called me" but not "alice asked the
|
||||||
@@ -359,9 +459,9 @@ authenticates as itself (its own `auth_token`); the `forwarded_for` field
|
|||||||
carries the originator's identity as context.
|
carries the originator's identity as context.
|
||||||
|
|
||||||
This is a protocol addition — a field on the `call.requested` payload and
|
This is a protocol addition — a field on the `call.requested` payload and
|
||||||
on `OperationContext`. It's in or it's out; it can't be bolted on later
|
on `OperationContext`. It's included in the ADR-029 migration or a
|
||||||
without a protocol change. The recommendation is to include it from the
|
companion task — the `from_call` handler is being rewritten anyway, and the
|
||||||
start.
|
`OperationContext` struct is being touched.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -372,15 +472,18 @@ start.
|
|||||||
```
|
```
|
||||||
alknet-core (lean — no SQLite, no honker)
|
alknet-core (lean — no SQLite, no honker)
|
||||||
├── IdentityProvider trait (the auth repo trait — already exists)
|
├── IdentityProvider trait (the auth repo trait — already exists)
|
||||||
|
├── CredentialStore trait (the encrypted-credentials repo trait — needs adding)
|
||||||
├── Identity, AuthToken, AuthContext (the auth types — already exist)
|
├── Identity, AuthToken, AuthContext (the auth types — already exist)
|
||||||
├── AccessControl, AccessResult (the ACL check — already exists)
|
├── AccessControl, AccessResult (the ACL check — already exists)
|
||||||
└── (no PeerStore trait — adapter-internal, not core)
|
├── ConfigIdentityProvider (in-memory adapter — needs PeerEntry update)
|
||||||
|
├── InMemoryCredentialStore (in-memory adapter — needs building)
|
||||||
|
└── PeerEntry (config model for decoupled id — needs adding to AuthPolicy)
|
||||||
|
|
||||||
Storage-consuming crates (each owns its SQLite + honker):
|
Storage-consuming crates (each owns its SQLite + honker):
|
||||||
├── alknet-filesystem — path-tree tables (tree, not graph; POC-proven)
|
├── alknet-peer-store-sqlite — SqliteIdentityProvider (peers table + honker)
|
||||||
├── peer registry — peers table (KV; implements IdentityProvider)
|
├── alknet-credential-store-sqlite — SqliteCredentialStore (credentials table + honker)
|
||||||
├── provider credentials — credentials table (KV; encrypted by vault)
|
├── alknet-filesystem — path-tree tables (tree, not graph; POC-proven)
|
||||||
└── alknet-graphs (future) — metagraph tables (graph-shaped problems)
|
└── alknet-graphs — metagraph tables (graph-shaped problems: ACL delegation, workflows, taskgraph)
|
||||||
|
|
||||||
alknet-call (lean — no SQLite, no honker, no storage traits)
|
alknet-call (lean — no SQLite, no honker, no storage traits)
|
||||||
├── Uses IdentityProvider (the trait, not the adapter)
|
├── Uses IdentityProvider (the trait, not the adapter)
|
||||||
@@ -391,18 +494,20 @@ alknet-call (lean — no SQLite, no honker, no storage traits)
|
|||||||
|
|
||||||
### What goes where
|
### What goes where
|
||||||
|
|
||||||
| Concern | Where it lives | Shape |
|
| Concern | Where it lives | Shape | Status |
|
||||||
|---------|---------------|-------|
|
|---------|---------------|-------|--------|
|
||||||
| Auth repo trait (`IdentityProvider`) | alknet-core | Trait (already exists) |
|
| Auth repo trait (`IdentityProvider`) | alknet-core | Trait | Exists |
|
||||||
| Auth adapters (Config, SQLite, future Redis/remote) | Adapter crates or assembly layer | Implements `IdentityProvider` |
|
| Credential repo trait (`CredentialStore`) | alknet-core | Trait | Needs adding |
|
||||||
| Per-node ACL check (`AccessControl::check`) | alknet-core (already exists) | Table-shaped: scope/resource match |
|
| In-memory auth adapter (`ConfigIdentityProvider`) | alknet-core | Config-backed | Needs `PeerEntry` update |
|
||||||
| Peer identity storage (PeerStore) | Adapter crate (adapter-internal) | `peers` table |
|
| In-memory credential adapter (`InMemoryCredentialStore`) | alknet-core | HashMap-backed | Needs building |
|
||||||
| Filesystem path tree + bucket ACL | alknet-filesystem | Specialized tables (POC-proven) |
|
| SQLite auth adapter (`SqliteIdentityProvider`) | `alknet-peer-store-sqlite` | `peers` table + honker | Needs building |
|
||||||
| Provider credentials (encrypted) | Adapter crate or assembly layer | `credentials` table (vault encrypts) |
|
| SQLite credential adapter (`SqliteCredentialStore`) | `alknet-credential-store-sqlite` | `credentials` table + honker | Needs building |
|
||||||
| ACL delegation graph (future) | alknet-graphs (metagraph) | Graph (traversal, scope narrowing) |
|
| Per-node ACL check (`AccessControl::check`) | alknet-core | Table-shaped: scope/resource match | Exists |
|
||||||
| Workflows / flowgraph (future) | alknet-graphs (metagraph) | Graph (DAG) |
|
| Filesystem path tree + bucket ACL | alknet-filesystem | Specialized tables (POC-proven) | POC done, crate needs building |
|
||||||
| Taskgraph (future) | alknet-graphs (metagraph) | Graph (dependency DAG) |
|
| ACL delegation graph | alknet-graphs (metagraph) | Graph (traversal, scope narrowing) | Needs building when delegation is needed |
|
||||||
| Forwarded-for identity | alknet-call (protocol field) | Metadata on `call.requested` + `OperationContext` |
|
| Workflows / flowgraph | alknet-graphs (metagraph) | Graph (DAG) | Needs building when workflows are needed |
|
||||||
|
| Taskgraph | alknet-graphs (metagraph) | Graph (dependency DAG) | Needs building when taskgraph is needed |
|
||||||
|
| Forwarded-for identity | alknet-call (protocol field) | Metadata on `call.requested` + `OperationContext` | Needs adding |
|
||||||
|
|
||||||
### What the old spec had that we're dropping
|
### What the old spec had that we're dropping
|
||||||
|
|
||||||
@@ -411,8 +516,8 @@ alknet-call (lean — no SQLite, no honker, no storage traits)
|
|||||||
| Multi-tenant (system.db + tenant.db) | Dropped | Each tenant gets its own complete setup (own ACL, ops, DB). Simpler, no cross-tenant complexity. |
|
| Multi-tenant (system.db + tenant.db) | Dropped | Each tenant gets its own complete setup (own ACL, ops, DB). Simpler, no cross-tenant complexity. |
|
||||||
| `secrets/` module (HD derivation, secret service) | Replaced by alknet-vault | The vault already handles encryption/decryption (ADR-018/019/020/025/026). Storage just stores the `EncryptedData` blob. |
|
| `secrets/` module (HD derivation, secret service) | Replaced by alknet-vault | The vault already handles encryption/decryption (ADR-018/019/020/025/026). Storage just stores the `EncryptedData` blob. |
|
||||||
| Metagraph as the foundation | Demoted to tool | SQLite+honker is the foundation. Metagraph is one tool on it, for graph-shaped problems. Tables are another tool, for table-shaped problems. |
|
| Metagraph as the foundation | Demoted to tool | SQLite+honker is the foundation. Metagraph is one tool on it, for graph-shaped problems. Tables are another tool, for table-shaped problems. |
|
||||||
| `alknet-storage` as one crate | Split | The storage-consuming concerns are separate (filesystem, peer registry, graphs). No single "storage" crate. |
|
| `alknet-storage` as one crate | Split | The storage-consuming concerns are separate (peer store, credential store, filesystem, graphs). No single "storage" crate. |
|
||||||
| Accounts/organizations/multi-tenant identity | Deferred | The v1 need is a `peers` table (PeerId → fingerprint + scopes). The full account/org model is a future adapter. |
|
| Accounts/organizations/multi-tenant identity | Dropped | The need is a `peers` table (PeerId → fingerprint + scopes). The full account/org model is over-engineering for the current use case. |
|
||||||
| `alknet-flowgraph` as a separate crate | Folded into alknet-graphs | The metagraph + petgraph interop are one crate for graph-shaped problems. |
|
| `alknet-flowgraph` as a separate crate | Folded into alknet-graphs | The metagraph + petgraph interop are one crate for graph-shaped problems. |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -428,7 +533,7 @@ check is `AccessControl::check(identity)` — a flat scope-match, not a graph
|
|||||||
traversal. This is fast, indexable, and correct for the current model (no
|
traversal. This is fast, indexable, and correct for the current model (no
|
||||||
delegation).
|
delegation).
|
||||||
|
|
||||||
### Delegation is graph-shaped (future)
|
### Delegation is graph-shaped
|
||||||
|
|
||||||
When delegation is needed ("A delegates to B with narrowed scopes, B
|
When delegation is needed ("A delegates to B with narrowed scopes, B
|
||||||
delegates to C with further narrowing"), the delegation chain is a graph
|
delegates to C with further narrowing"), the delegation chain is a graph
|
||||||
@@ -512,81 +617,104 @@ own storage. The ACL doesn't inspect operation input; the handler does.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. What This Means for the Immediate Path
|
## 10. Build Order
|
||||||
|
|
||||||
### ADR-029 migration (now)
|
This is the concrete sequence, not a deferral. Each item is a design
|
||||||
|
commitment that needs to be built. The order is dependency-driven, not
|
||||||
|
priority-driven — earlier items unblock later ones.
|
||||||
|
|
||||||
The peer-graph routing migration uses the UUID workaround (no storage). This
|
### Tier 1: Core repo traits and config model (unblocks everything)
|
||||||
document doesn't change that. But it establishes the pattern for when
|
|
||||||
storage arrives:
|
|
||||||
|
|
||||||
1. **ADR-029 migration** (now) — UUID PeerId, no storage, in-memory peer
|
1. **`PeerEntry` in `AuthPolicy`** — replace `authorized_fingerprints:
|
||||||
overlays. `IdentityProvider` is `ConfigIdentityProvider` (in-memory).
|
HashSet<String>` with `peers: Vec<PeerEntry>` (peer_id, fingerprint,
|
||||||
2. **Peer registry** (when key rotation / durable peer attribution is
|
scopes, resources). Update `ConfigIdentityProvider` to resolve
|
||||||
needed) — `peers` table + honker, implements `IdentityProvider`, replaces
|
fingerprint → `PeerEntry` → `Identity { id: peer_id, ... }`. This is the
|
||||||
`ConfigIdentityProvider`. The call protocol's `Dispatcher` uses
|
id-fingerprint decoupling (OQ-33). Without this, the ACL keys on the
|
||||||
`IdentityProvider` as today — no change. The `PeerCompositeEnv` uses
|
fingerprint and breaks on key rotation.
|
||||||
`PeerId` (= `Identity.id` from the adapter) — no change to routing.
|
|
||||||
3. **alknet-graphs** (when ACL delegation / workflows / taskgraph are
|
|
||||||
needed) — metagraph crate, built on the same SQLite+honker pattern. For
|
|
||||||
graph-shaped problems only.
|
|
||||||
|
|
||||||
Each step is independent. The migration doesn't wait for storage. Storage
|
2. **`CredentialStore` trait in core** — the repo trait for encrypted
|
||||||
doesn't wait for the metagraph. The metagraph doesn't wait for the filesystem
|
provider credentials. `InMemoryCredentialStore` adapter (HashMap-backed)
|
||||||
(which already has its own tables).
|
for tests and config-loaded deployments.
|
||||||
|
|
||||||
### What goes into specs next (after this doc is reviewed)
|
These are core changes — no SQLite, no honker, no new crates. They fix the
|
||||||
|
id-fingerprint coupling and establish the credential repo pattern.
|
||||||
|
|
||||||
1. **`IdentityProvider` as the auth repo trait** — make the repo framing
|
### Tier 2: SQLite adapters (enables persistence)
|
||||||
explicit in `auth.md` and the `IdentityProvider` doc. No trait change;
|
|
||||||
just documenting the pattern.
|
|
||||||
2. **`forwarded_for` field** — add to `call-protocol.md` (the
|
|
||||||
`call.requested` payload schema) and `operation-registry.md`
|
|
||||||
(`OperationContext`). `AccessControl::check` signature unchanged.
|
|
||||||
3. **Per-node ACL framing** — add to `client-and-adapters.md` and
|
|
||||||
`operation-registry.md` as the cross-node extension of the existing
|
|
||||||
`AccessControl` model. No "trusted" flag.
|
|
||||||
4. **OQ-34 update** — record the repo-pattern framing and the decomposition
|
|
||||||
(SQLite+honker as pattern, metagraph as tool, `IdentityProvider` as the
|
|
||||||
core trait).
|
|
||||||
|
|
||||||
### What does NOT go into specs (stays in this research doc)
|
3. **`alknet-peer-store-sqlite`** — `SqliteIdentityProvider` backed by a
|
||||||
|
`peers` table + honker. Implements `IdentityProvider`. The assembly layer
|
||||||
|
wires it instead of `ConfigIdentityProvider` when persistence is needed.
|
||||||
|
The `peers` table schema is in §4. Honker `notify("peers:changed")` on
|
||||||
|
mutations for cache invalidation.
|
||||||
|
|
||||||
- The metagraph schema (GraphType/NodeType/EdgeType) — that's a future
|
4. **`alknet-credential-store-sqlite`** — `SqliteCredentialStore` backed by
|
||||||
`alknet-graphs` spec, not relevant to the current crates
|
a `credentials` table + honker. Implements `CredentialStore`. The
|
||||||
- The filesystem's path-tree schema — that's the filesystem crate's spec
|
assembly layer wires it when credentials need to persist across restarts.
|
||||||
- The full account/org identity model — deferred; the v1 need is a `peers`
|
|
||||||
table
|
These are new crates — each owns its SQLite file, attaches honker, defines
|
||||||
- The distributed-auth adapter (automerge/Redis) — a door the repo pattern
|
its schema. They implement the core traits.
|
||||||
opens; not designed
|
|
||||||
|
### Tier 3: Protocol and call crate (enables cross-node composition)
|
||||||
|
|
||||||
|
5. **ADR-029 migration** — peer-keyed overlays (`PeerCompositeEnv`), retire
|
||||||
|
`remote_safe`/`trusted_peer`, `PeerRef` routing, `AccessControl`-based
|
||||||
|
peer authorization. The `forwarded_for` field is added here (or in a
|
||||||
|
companion task) since `OperationContext` and the `from_call` handler are
|
||||||
|
being rewritten.
|
||||||
|
|
||||||
|
6. **`forwarded_for` field** — add to `call.requested` payload and
|
||||||
|
`OperationContext`. The `from_call` handler populates it; the dispatch
|
||||||
|
path makes it available; `AccessControl::check` ignores it. This is a
|
||||||
|
protocol addition that's included with the migration or done as a
|
||||||
|
companion task immediately after.
|
||||||
|
|
||||||
|
### Tier 4: Graph-shaped problems (enables ACL delegation, workflows, taskgraph)
|
||||||
|
|
||||||
|
7. **`alknet-graphs`** — the metagraph crate (GraphType/NodeType/EdgeType,
|
||||||
|
CRUD, schema validation, petgraph interop). Built on SQLite + honker.
|
||||||
|
This is built when the first graph-shaped consumer needs it — ACL
|
||||||
|
delegation, workflows, or taskgraph. Not built speculatively; built when
|
||||||
|
there's a graph-shaped problem to solve.
|
||||||
|
|
||||||
|
8. **ACL delegation graph** — a metagraph instance (PrincipalNode,
|
||||||
|
DelegatesEdge, scope narrowing). The `IdentityProvider` adapter traverses
|
||||||
|
it to compute effective scopes. Built when delegation is needed — not
|
||||||
|
before, not speculatively.
|
||||||
|
|
||||||
|
### What does NOT get built (dropped, not deferred)
|
||||||
|
|
||||||
|
- Multi-tenant (system.db + tenant.db) — dropped; each tenant gets its own
|
||||||
|
setup
|
||||||
|
- Accounts/organizations/multi-tenant identity — dropped; the `peers` table
|
||||||
|
is the model
|
||||||
|
- `secrets/` module — dropped; the vault handles encryption
|
||||||
|
- `alknet-storage` as one crate — dropped; split by concern
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Open Questions
|
## 11. Open Questions
|
||||||
|
|
||||||
1. **When does the `forwarded_for` field get added?** It's a protocol
|
1. **Does the peer registry SQLite adapter live in its own crate
|
||||||
addition (a field on `call.requested` and `OperationContext`). It's in
|
(`alknet-peer-store-sqlite`) or in the assembly layer?** The kepal
|
||||||
the ADR-029 migration or it's a separate protocol-change task. The
|
pattern suggests a separate crate (the adapter is reusable across
|
||||||
recommendation is to include it in the migration — the `from_call`
|
deployments). `ConfigIdentityProvider` lives in core (a simple impl);
|
||||||
handler is being rewritten anyway, and the `OperationContext` struct is
|
the SQLite adapter could live in a separate crate or in the assembly
|
||||||
being touched. Adding the field now is cheaper than a separate protocol
|
layer's binary. This is a packaging choice — the trait is in core either
|
||||||
change later.
|
way.
|
||||||
|
|
||||||
2. **Does the peer registry adapter live in its own crate or in the assembly
|
2. **Does the ACL delegation graph produce `Identity.scopes` at resolution
|
||||||
layer?** The `ConfigIdentityProvider` lives in alknet-core (a simple
|
time or at check time?** The recommendation in §8 is at resolution time
|
||||||
impl). The SQLite adapter could live in a `alknet-peer-store-sqlite`
|
(the `IdentityProvider` adapter traverses the delegation graph to compute
|
||||||
crate, or it could be in the assembly layer's binary (like a wiring
|
effective scopes, returns an `Identity` with them, and the check is
|
||||||
detail). The kepal pattern suggests a separate crate (the adapter is
|
flat). The alternative is lazy computation (the check triggers the
|
||||||
reusable across deployments). This is a two-way door — the trait is in
|
traversal). This is a design question for when the delegation graph is
|
||||||
core either way; the adapter's location is a packaging choice.
|
built — the current model has no delegation, so it's not blocking.
|
||||||
|
|
||||||
3. **Does the ACL delegation graph (future) produce `Identity.scopes` at
|
3. **Does the `CredentialStore` trait need a `list` method?** The current
|
||||||
resolution time or at check time?** The recommendation in §8 is at
|
design has `get`/`put`/`delete`. A `list` (list all providers) might be
|
||||||
resolution time (the `IdentityProvider` adapter traverses the delegation
|
needed for a management UI or for the assembly layer to enumerate
|
||||||
graph to compute effective scopes, returns an `Identity` with them, and
|
credentials at startup. Two-way door — add `list` when a consumer needs
|
||||||
the check is flat). But an alternative is lazy computation (the check
|
it.
|
||||||
triggers the traversal). This is a future question, not a v1 decision —
|
|
||||||
the current model has no delegation.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user