Synthesizes the multi-thread discussion that surfaced during the peer-graph routing research (ADR-029) and OQ-33/34 resolution. Three separate threads (peer identity, filesystem POC, old storage spec) converged on the same question: where does persistent state live in the alknet crate graph, and what's the shared infrastructure for it. Key commitments documented: - SQLite + honker is the foundation (pattern, not a crate — ~20 lines per consumer). The metagraph is one tool built on it, for graph-shaped problems. Direct tables are another tool, for table-shaped problems. - IdentityProvider is the auth repo trait (already exists in core, make the pattern explicit). Adapters implement it (Config, SQLite, future Redis/remote/automerge). PeerStore is adapter-internal, not core. - Per-node ACL, no 'trusted' flag. Each node authorizes its direct callers via AccessControl::check(identity). No global ACL, no replication. The hub authorizes the user; the spoke authorizes the hub. Same mechanism. - Forwarded-for identity as metadata, not authority. The from_call handler includes the original caller's identity in the call payload; the spoke's ACL authorizes the hub (direct caller), never the forwarded_for. The ACL check signature prevents misuse. - The ACL check stays table-shaped (flat scope match); the delegation graph (future) produces effective scopes at resolution time. They compose at the IdentityProvider boundary. - The hub proxy tangle: ACL (authorize), bucket routing (operation input), peer routing (PeerRef) are three separate layers. Bucket-level authorization is handler logic, not protocol logic. What the old spec had that's dropped: multi-tenant (each tenant gets own setup), secrets module (replaced by vault), metagraph-as-foundation (demoted to tool), single storage crate (split by concern), accounts/orgs (deferred — v1 is a peers table). Reference: kepal (/workspace/keypal) — TypeScript repo-pattern example (Storage interface + adapters) that alknet's IdentityProvider follows.
30 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-27 |
Storage and Auth Strategy
Status: Draft for iteration Date: 2026-06-27 Scope: Cross-cutting — storage decomposition, auth/ACL model, repo pattern, SQLite+honker as foundation, metagraph as tool. Synthesizes the discussion that surfaced during the peer-graph routing research (ADR-029) and OQ-33/34 resolution.
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 —
it's the research that will inform ADRs and spec amendments. The
implementation-relevant pieces (the forwarded_for field, the
IdentityProvider-as-repo framing) get folded into specs after review.
1. The Problem
Three separate threads converged on the same question: where does persistent state live in the alknet crate graph, and what's the shared infrastructure for it?
- 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, surviving key rotation and restarts. The UUID workaround is ephemeral; the real solution is a store.
- Filesystem (POC-validated) — SQLite + honker + iroh-blobs as the three-layer stack for path-tree metadata, content-addressed blobs, and transactional notify-on-commit. 24 tests across two POC crates.
- The old
alknet-storagespec (alknet-main) — a single crate doing metagraph, identity, ACL, secrets, and honker integration. Designed before the vault existed, before ADR-029, before the filesystem POC. Has residual issues: multi-tenant complexity, secrets module that's now the vault, metagraph-as-foundation rather than metagraph-as-tool.
The common thread: SQLite via honker is the right local persistence layer for all three, and the metagraph model is the right shape for some of the 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 everything through the same abstraction.
2. The Principle: Right Tool for the Right Shape
The metagraph (GraphType → NodeType → EdgeType → Graph → Node → Edge) is a generalized graph store. It's the right tool for genuinely graph-shaped problems: ACL delegation chains, workflows, task dependency DAGs, call composition trees. It is the wrong tool for things that aren't graph-shaped:
| Data | Shape | Right tool |
|---|---|---|
| Peer identity → crypto material + scopes | Key-value (flat table) | peers table with typed columns |
| Filesystem path tree | Tree (degenerate graph) | Specialized path-tree tables (recursive CTE, proven by POC) |
| Provider credentials (encrypted blobs) | Key-value | credentials table |
| ACL delegation chains | Graph (traversal, narrowing) | Metagraph |
| Workflows / flowgraph | Graph (DAG, type compatibility) | Metagraph |
| Taskgraph | Graph (dependency DAG) | Metagraph |
| Operation specs | Flat records with typed fields | Table (or in-memory registry, as today) |
Forcing table-shaped data through the metagraph adds overhead (JSON Schema validation on every node, graph traversal for what should be an indexed lookup) without benefit. The filesystem POC proved this empirically: the path tree uses specialized tables with a recursive CTE, and it's sub- millisecond. The same data in a metagraph would be a graph traversal per resolve — slower, more complex, no upside.
The principle: SQLite + honker is the foundation. The metagraph is one tool built on it, for graph-shaped problems. Direct tables are another tool, for table-shaped problems. Each consumer picks the right tool.
3. SQLite + Honker as Foundation (Pattern, Not Crate)
The filesystem POC established the integration pattern:
honker_core::apply_default_pragmas(conn)?; // WAL, synchronous=NORMAL
honker_core::attach_notify(conn)?; // notify() SQL function
honker_core::attach_honker_functions(conn)?; // enqueue, claim, lock, stream, cron
honker_core::bootstrap_honker_schema(conn)?; // queue/stream/scheduler tables
This is ~20 lines of setup per consumer. Each consumer that wants its own
tables does this on its own rusqlite connection. The critical property: the
honker functions live on the same connection as the data tables, so writes
and notifications are atomic in one transaction (the transactional-outbox
pattern, built in). This is honker-core (attach to your connection), not
honker (manages its own connection) — the POC documented this distinction.
This is a pattern, not a crate. Packaging ~20 lines of setup as a shared
crate adds a dependency boundary for no gain. Each consumer opens its own
SQLite file, attaches honker, defines its schema. A setup_honker(conn)
helper function (in a shared utility, or just copy-pasted) is enough.
Why SQLite, not a "real database"
SQLite is an application file format, not just a database. The filesystem POC's insight: BLOBs < 100KB are faster inline in SQLite than as filesystem files; atomic transactions over metadata independent of content; the schema is the documentation. Each consumer gets a local, crash-safe, queryable file — not a database server to operate.
The core crates (alknet-core, alknet-call) stay DB-free. The storage- consuming crates (filesystem, peer registry, graphs) each own their SQLite file. The assembly layer wires them together.
What honker adds
| Feature | Use case |
|---|---|
notify / listen |
Ephemeral pub/sub — "ACL entry changed, invalidate cache" |
stream_publish / subscribe |
Durable pub/sub — "peer identity updated, propagate" |
queue / claim / ack |
Task queue — "orphaned write session cleanup" |
lock_acquire / lock_release |
Named locks — "writer coordination on a path" |
scheduler |
Periodic tasks — "session cleanup, audit log pruning" |
The key integration: every mutation is atomic with its notification. A
peers table update + notify("peers:changed", peer_id) commit together.
A downstream consumer (e.g., the call protocol's IdentityProvider cache)
wakes on commit, not on poll.
4. The Repo Pattern for Auth
The existing pattern (make it explicit)
alknet-core already has the repo pattern: IdentityProvider is a trait
with two methods (resolve_from_fingerprint, resolve_from_token), one
adapter (ConfigIdentityProvider, backed by ArcSwap<DynamicConfig>), and
one consumer (the call protocol's Dispatcher). This is a repo trait — it
abstracts the what (resolve an identity from a credential) from the how
(in-memory config, SQLite, Redis, remote service).
Make this explicit. IdentityProvider is the auth repo trait in core.
Adapters implement it. The assembly layer wires the adapter. Downstream
crates consume the trait, not the adapter.
Why this matters beyond the call crate
Downstream crates that don't use the call protocol still need auth. A crate that exposes operations over HTTP (alknet-http) or a service with no protocol at all still needs to resolve identities and check ACL. If the auth layer is 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 call crate is one consumer of auth, not the owner of it.
The distributed-auth door
If the repo trait is clean, someone can wire an adapter that syncs via 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
The TypeScript project kepal is a clean example of this
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
Storage), ConfigIdentityProvider is the in-memory adapter (like kepal's
MemoryStore), the SQLite peer registry is the real adapter (like kepal's
RedisStore/DrizzleStore), and the assembly layer wires the adapter (like
kepal's Manager constructor). The shapes map cleanly.
PeerStore: adapter-internal, not core
A PeerStore trait (save/find/update/delete peer records) is an
adapter-internal detail, not a core trait. The core trait is
IdentityProvider. The SQLite adapter implements IdentityProvider by
delegating to a PeerStore internally. The trait boundary that matters for
cross-crate sharing is IdentityProvider, not PeerStore.
This keeps core lean: one auth trait (IdentityProvider), not two. The
store trait lives in the adapter crate (or the assembly layer), where it's
an implementation detail. If a future adapter (Redis, remote service) needs
a different internal store shape, it's free to define one — the core contract
is IdentityProvider, not the store.
5. Per-Node ACL, No "Trusted" Flag
The model
Each node has its own ACL. A node's ACL answers one question: is this
caller authorized to call this operation? The caller is whoever
authenticated to the connection — resolved by IdentityProvider from the
TLS fingerprint or auth_token, checked by AccessControl::check(identity).
No "trusted" flag, no bypass, no special mode.
This is the existing mechanism, restated for the cross-node case. The call
protocol's dispatch path (registration.rs:128-140) already runs
AccessControl::check against the caller's Identity. For a remote peer's
call, the caller's Identity is the peer's resolved identity. Same check,
same mechanism, no new concept.
Why no "trusted=true"
A generic "trusted" flag is a blanket authorization bypass — the exact
anti-pattern that ADR-015 was written to kill (it replaced trusted: true
with the authority-switch model). There is no circumstance where a generic
"skip the security check" flag is the right answer in a reasonably secure
system. If a caller is authorized, the ACL says so. If the ACL doesn't say
so, the caller isn't authorized. There's no third state.
The cross-node case
When a hub forwards to a spoke (via from_call), the spoke authenticates
the hub (resolves the hub's identity from the connection), and checks its
ACL: "is this identity authorized to call this operation?" The answer is
yes or no, based on the hub's identity and the op's AccessControl. Same
mechanism, same check, no special-casing.
End user ──calls──> Hub ──forwards as hub──> Spoke (docker service)
│ │
hub's ACL spoke's ACL
(user → hub ops) (hub → spoke ops)
The hub's ACL checked the end user. The spoke's ACL checked the hub. Two independent authorization decisions, same mechanism, no replication. The hub isn't "trusted" by the spoke — the hub is authorized by the spoke's ACL, the same way any caller is authorized.
The service-to-service pattern
This is the same principle as: a database server authorizes the application server; it doesn't need to know about every end user the app server authenticated. The application server is the authorization boundary. In alknet, each node is an authorization boundary for its direct callers.
The docker service example: the service exposes /docker/start. It's
reachable directly (end users connect and call it) or through a hub (the
hub imports via from_call, re-exposes, forwards). The docker service's
ACL lists the principals that call it directly — either end users (direct
topology) or the hub (proxied topology). It doesn't need to know about the
hub's end users. The hub's ACL handles end-user authorization.
No global ACL, no replication
Each node's ACL is local — in its own SQLite file (when storage arrives), in
its own peers table, checked by its own AccessControl. There is no
global ACL, no cross-service ACL replication. When a user's key rotates, the
hub's peers table updates her fingerprint. The spoke's peers table is
unchanged — it only knows about the hub. When the hub's key rotates, the
spoke's peers table updates the hub's fingerprint — a single 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.
6. Forwarded-For Identity (Metadata, Not Authority)
The question
When a hub forwards a call to a spoke, should the spoke know who initiated the call (the end user), or just who called it (the hub)?
Without forwarded-for (what the implementation does today): the spoke
sees the hub as the caller. It authorizes the hub. It logs "the hub called
/docker/start." If the spoke needs to audit "who actually initiated this,"
it can't — that information is at the hub.
With forwarded-for: the hub includes the original caller's identity in
the call.requested payload. The spoke can log it, use it for per-user
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
informational, not authoritative.
The recommendation: add it, as metadata
The forwarded-for identity should be added as a protocol-level field, not as an afterthought. Reasoning:
-
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 hub to call me." For debugging, billing, and abuse investigation, the originator matters.
-
It's metadata, not authority. The forwarded-for identity goes in the call's metadata (or a dedicated
forwarded_forfield), not as theauth_token. The spoke's dispatch path makes it available onOperationContextbutAccessControl::checknever uses it — it always authorizes the direct caller's identity. This keeps it from becoming an authorization bypass. -
The ACL check signature prevents misuse.
AccessControl::checktakesOption<&Identity>(the direct caller's identity).forwarded_foris a separate field onOperationContext(Option<Identity>). The ACL check signature doesn't accept it. If someone wants to ACL on the forwarded-for identity, they'd have to change theAccessControl::checksignature — a visible, reviewable change, not a quiet flag flip. -
Without it, the leaf service is blind to the originator. If the spoke needs to rate-limit per-user (not per-hub), or log who triggered a container start, it can't. The hub would have to proxy and track everything, which defeats the point of direct service composition.
Protocol shape
The call.requested payload gains an optional forwarded_for field:
{
"operationId": "/docker/start",
"input": { ... },
"auth_token": "alk_...", // the direct caller's token (the hub's)
"forwarded_for": { // the original caller (the end user's)
"id": "alice-fingerprint",
"scopes": ["fs:read", "docker:start"]
}
}
The dispatch path populates OperationContext:
pub struct OperationContext {
// ... existing fields ...
pub identity: Option<Identity>, // the direct caller (authorized by ACL)
pub forwarded_for: Option<Identity>, // the original caller (metadata only)
}
AccessControl::check(identity.as_ref()) — unchanged. The forwarded_for
field is available to handlers for logging, auditing, rate-limiting, but
never to the ACL.
The from_call handler's responsibility
The hub's from_call forwarding handler populates forwarded_for with the
end user's identity (from the hub's OperationContext.identity) when it
constructs the call.requested payload to send to the spoke. The hub
authenticates as itself (its own auth_token); the forwarded_for field
carries the originator's identity as context.
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
without a protocol change. The recommendation is to include it from the
start.
7. The Decomposition
Crate boundaries
alknet-core (lean — no SQLite, no honker)
├── IdentityProvider trait (the auth repo trait — already exists)
├── Identity, AuthToken, AuthContext (the auth types — already exist)
├── AccessControl, AccessResult (the ACL check — already exists)
└── (no PeerStore trait — adapter-internal, not core)
Storage-consuming crates (each owns its SQLite + honker):
├── alknet-filesystem — path-tree tables (tree, not graph; POC-proven)
├── peer registry — peers table (KV; implements IdentityProvider)
├── provider credentials — credentials table (KV; encrypted by vault)
└── alknet-graphs (future) — metagraph tables (graph-shaped problems)
alknet-call (lean — no SQLite, no honker, no storage traits)
├── Uses IdentityProvider (the trait, not the adapter)
├── PeerCompositeEnv keyed by PeerId (= Identity.id from IdentityProvider)
├── AccessControl::check(identity) for per-node ACL
└── from_call handler authenticates as the hub, forwards-for as metadata
What goes where
| Concern | Where it lives | Shape |
|---|---|---|
Auth repo trait (IdentityProvider) |
alknet-core | Trait (already exists) |
| Auth adapters (Config, SQLite, future Redis/remote) | Adapter crates or assembly layer | Implements IdentityProvider |
Per-node ACL check (AccessControl::check) |
alknet-core (already exists) | Table-shaped: scope/resource match |
| Peer identity storage (PeerStore) | Adapter crate (adapter-internal) | peers table |
| Filesystem path tree + bucket ACL | alknet-filesystem | Specialized tables (POC-proven) |
| Provider credentials (encrypted) | Adapter crate or assembly layer | credentials table (vault encrypts) |
| ACL delegation graph (future) | alknet-graphs (metagraph) | Graph (traversal, scope narrowing) |
| Workflows / flowgraph (future) | alknet-graphs (metagraph) | Graph (DAG) |
| Taskgraph (future) | alknet-graphs (metagraph) | Graph (dependency DAG) |
| Forwarded-for identity | alknet-call (protocol field) | Metadata on call.requested + OperationContext |
What the old spec had that we're dropping
| Old spec | Status | Why |
|---|---|---|
| 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. |
| 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. |
| 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. |
alknet-flowgraph as a separate crate |
Folded into alknet-graphs | The metagraph + petgraph interop are one crate for graph-shaped problems. |
8. The ACL Split: Check Stays Table, Delegation Is Graph
The current ACL is table-shaped
AccessControl on OperationSpec is required_scopes (AND-gate),
required_scopes_any (OR-gate), resource_type/resource_action. Identity
has scopes: Vec<String> and resources: HashMap<String, Vec<String>>. The
check is AccessControl::check(identity) — a flat scope-match, not a graph
traversal. This is fast, indexable, and correct for the current model (no
delegation).
Delegation is graph-shaped (future)
When delegation is needed ("A delegates to B with narrowed scopes, B delegates to C with further narrowing"), the delegation chain is a graph traversal — you walk the chain computing the effective scope set. This is where the metagraph pays off (PrincipalNode, DelegatesEdge, scope narrowing).
But the check stays table-shaped even with delegation: the delegation
graph produces the effective Identity.scopes (the graph's output); the ACL
check is still "does the effective scope set satisfy the op's requirements?"
(a flat join). The graph and the table compose — the graph produces the
scopes, the table checks them.
Don't force the check through the graph
The temptation is to make AccessControl::check traverse the delegation
graph. Don't. The check is a flat scope-match — keep it that way. The
delegation graph is a separate concern (producing effective scopes), and it
lives in alknet-graphs (metagraph). The check lives in core (table). They
compose at the IdentityProvider boundary: the adapter resolves the identity
(possibly by traversing the delegation graph to compute effective scopes),
returns an Identity with the effective scopes, and the check is a flat
match against that Identity.
This matches the "don't use a screwdriver to hammer a nail" principle: the check is table-shaped, the delegation is graph-shaped, and forcing either through the other's shape is worse.
9. The Hub Proxy Tangle (Resolved)
The tangle
A hub can "have a filesystem" two ways:
- In-process — the hub's binary loads
alknet-filesystem. The filesystem's SQLite is local. The hub's call protocol dispatches/fs/readFiledirectly to the filesystem handler. No network. - Proxied — the filesystem runs on a spoke. The hub imports the spoke's
ops via
from_call. The hub'sfrom_callhandler forwards over QUIC. The spoke's call protocol dispatches to its own filesystem handler.
These are different deployment topologies for the same libraries. The libraries don't change; the assembly does.
The three concerns that got conflated
- ACL — who can call the operation? The hub's ACL authorizes the user. The spoke's ACL authorizes the hub. (Per-node ACL, same mechanism.)
- Bucket routing — which bucket is the operation targeting? The bucket
is a parameter in the operation input (
{ "bucket": "alice-files", "path": "hello.txt" }). It's not an ACL concern — it's operation input. - Peer routing — which spoke hosts the operation? This is
PeerRef::Specific(ADR-029) — the hub's composition env routes to the right peer.
These are three separate decisions at three separate layers:
User calls hub's /fs/readFile with { bucket: "alice-files", path: "hello.txt" }
→ hub's ACL: is this user authorized to call /fs/readFile? (AccessControl::check)
→ hub's composition env: which peer serves /fs/readFile? (PeerRef routing)
→ hub's from_call handler: forward { bucket, path } to that peer
→ spoke's ACL: is the hub authorized to call /fs/readFile? (AccessControl::check)
→ spoke's filesystem handler: read path from bucket (operation logic + bucket ACL)
Bucket-level authorization
The call protocol's ACL is coarse: "can this identity call /fs/readFile?"
It doesn't know about buckets. The bucket is in the operation input. The
handler checks bucket-level authorization — the filesystem handler reads
ctx.identity, reads the input's bucket field, and checks its own bucket
ACL (a bucket_acl table in the filesystem's SQLite: "is this identity
authorized for this bucket?"). This is application logic — the filesystem
owns its bucket authorization. The call protocol's ACL is the coarse gate;
the handler is the fine gate.
This keeps the call protocol's ACL simple and fast (a scope/resource check), and lets each service define its own fine-grained authorization against its own storage. The ACL doesn't inspect operation input; the handler does.
10. What This Means for the Immediate Path
ADR-029 migration (now)
The peer-graph routing migration uses the UUID workaround (no storage). This document doesn't change that. But it establishes the pattern for when storage arrives:
- ADR-029 migration (now) — UUID PeerId, no storage, in-memory peer
overlays.
IdentityProviderisConfigIdentityProvider(in-memory). - Peer registry (when key rotation / durable peer attribution is
needed) —
peerstable + honker, implementsIdentityProvider, replacesConfigIdentityProvider. The call protocol'sDispatcherusesIdentityProvideras today — no change. ThePeerCompositeEnvusesPeerId(=Identity.idfrom the adapter) — no change to routing. - 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 doesn't wait for the metagraph. The metagraph doesn't wait for the filesystem (which already has its own tables).
What goes into specs next (after this doc is reviewed)
IdentityProvideras the auth repo trait — make the repo framing explicit inauth.mdand theIdentityProviderdoc. No trait change; just documenting the pattern.forwarded_forfield — add tocall-protocol.md(thecall.requestedpayload schema) andoperation-registry.md(OperationContext).AccessControl::checksignature unchanged.- Per-node ACL framing — add to
client-and-adapters.mdandoperation-registry.mdas the cross-node extension of the existingAccessControlmodel. No "trusted" flag. - OQ-34 update — record the repo-pattern framing and the decomposition
(SQLite+honker as pattern, metagraph as tool,
IdentityProvideras the core trait).
What does NOT go into specs (stays in this research doc)
- The metagraph schema (GraphType/NodeType/EdgeType) — that's a future
alknet-graphsspec, not relevant to the current crates - The filesystem's path-tree schema — that's the filesystem crate's spec
- The full account/org identity model — deferred; the v1 need is a
peerstable - The distributed-auth adapter (automerge/Redis) — a door the repo pattern opens; not designed
11. Open Questions
-
When does the
forwarded_forfield get added? It's a protocol addition (a field oncall.requestedandOperationContext). It's in the ADR-029 migration or it's a separate protocol-change task. The recommendation is to include it in the migration — thefrom_callhandler is being rewritten anyway, and theOperationContextstruct is being touched. Adding the field now is cheaper than a separate protocol change later. -
Does the peer registry adapter live in its own crate or in the assembly layer? The
ConfigIdentityProviderlives in alknet-core (a simple impl). The SQLite adapter could live in aalknet-peer-store-sqlitecrate, or it could be in the assembly layer's binary (like a wiring detail). The kepal pattern suggests a separate crate (the adapter is reusable across deployments). This is a two-way door — the trait is in core either way; the adapter's location is a packaging choice. -
Does the ACL delegation graph (future) produce
Identity.scopesat resolution time or at check time? The recommendation in §8 is at resolution time (theIdentityProvideradapter traverses the delegation graph to compute effective scopes, returns anIdentitywith them, and the check is flat). But an alternative is lazy computation (the check triggers the traversal). This is a future question, not a v1 decision — the current model has no delegation.
References
- ADR-014: Secret Material Flow and Capability Injection (the no-env-vars invariant)
- ADR-015: Privilege Model and Authority Context (the authority-switch model
that replaced
trusted: true) - ADR-017: Call Protocol Client and Adapter Contract (the
from_callforwarding handler) - ADR-018/019/020/025/026: The vault crate (handles encryption/decryption;
storage stores the
EncryptedDatablob) - ADR-029: Peer-Graph Routing Model (peer-keyed overlays,
PeerRefrouting,AccessControl-based peer authorization) - OQ-33: PeerId — logical id, not crypto identity
- OQ-34: Persistent peer registry (the storage dimension)
docs/research/alknet-call-peer-routing/findings.md— the peer-graph routing research that surfaced the storage questiondocs/research/alknet-filesystem/poc-summary.md— the filesystem POC that validated SQLite + honker + iroh-blobs/workspace/@alkdev/alknet-main/docs/architecture/storage.md— the old storage spec (residual issues documented in §7)/workspace/@alkdev/alknet-main/docs/research/storage.md— the old storage research (metagraph, identity, ACL, honker integration)/workspace/keypal— TypeScript repo-pattern reference for API key management (Storage interface + adapters, the pattern alknet'sIdentityProviderfollows)/workspace/honker— SQLite extension with pub/sub, streams, queues, locks, scheduler (honker-corefor the attach-to-your-connection pattern)- https://sqlite.org/appfileformat.html — SQLite as an application file format