docs: write Phase 0 architecture foundation — ADRs 026-034, spec docs, and task updates

Phase 0a — ADRs (9 new):
- ADR-026: Transport/interface separation (three-layer model)
- ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI)
- ADR-028: Auth as irpc service (AuthProtocol behind feature flag)
- ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core)
- ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle)
- ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware)
- ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries)
- ADR-033: OperationEnv universal composition (three dispatch paths)
- ADR-034: Head/worker terminology (replace hub/spoke)

Phase 0b — New spec documents (7):
- identity.md, services.md, interface.md, configuration.md,
  storage.md, flowgraph.md, secret-service.md

Updated existing docs:
- auth.md: reference identity.md for canonical definitions, add AuthProtocol
- open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25
- README.md: add all new docs, ADRs 026-034

Marked 19 architecture tasks as completed.
This commit is contained in:
2026-06-07 09:32:58 +00:00
parent 84f16d66e7
commit 19b3d3a078
38 changed files with 2750 additions and 101 deletions

View File

@@ -0,0 +1,189 @@
---
status: draft
last_updated: 2026-06-07
---
# Identity
## What
The `Identity` type and `IdentityProvider` trait are the core abstractions for
authentication and authorization in alknet. `Identity` is the unified result of
auth verification — whether via SSH public key, signed timestamp token, or
database lookup. `IdentityProvider` is the trait that resolves credentials to an
`Identity`, decoupling alknet-core from any specific identity storage.
## Why
Auth, forwarding policy, and call protocol all need to know who is making a
request and what they are authorized to do. Without `Identity` in core, each
subsystem would define its own identity type, leading to duplication and
conversion boilerplate. Without `IdentityProvider` as a trait, alknet-core
would either hardcode config-file-based auth or take a database dependency —
neither acceptable for a library crate.
The `IdentityProvider` trait exists because the same auth verification concept
needs two implementations: `ConfigIdentityProvider` for minimal deployments (all
keys in memory via ArcSwap) and `StorageIdentityProvider` for production (SQLite
lookup via `peer_credentials` and ACL graph). The trait is the contract; the
backing store is pluggable.
## Architecture
### Identity Struct
```rust
pub struct Identity {
pub id: String, // Fingerprint or account UUID
pub scopes: Vec<String>, // e.g., ["relay:connect", "service:gitea:read"]
pub resources: HashMap<String, Vec<String>>, // e.g., {"service": ["gitea", "registry"]}
}
```
The `id` field serves dual purpose:
- **Config-based auth** (`ConfigIdentityProvider`): holds the Ed25519 key
fingerprint (e.g., `SHA256:abc123...`)
- **Database-backed auth** (`StorageIdentityProvider`): holds the account UUID
from the `accounts` table
This keeps the type simple while accommodating both auth paths. Downstream
consumers (forwarding policy, call protocol ACL checks) use `scopes` and
`resources` without knowing whether the identity came from a config file or a
database.
### IdentityProvider Trait
```rust
pub trait IdentityProvider: Send + Sync + 'static {
/// Resolve an SSH public key fingerprint to an identity.
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity>;
/// Resolve an auth token to an identity.
/// Returns None if the token is invalid, expired, or the key is not authorized.
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity>;
}
```
Both SSH key auth and token auth resolve to the same `Identity` type. The trait
lives in `alknet_core::auth`.
### ConfigIdentityProvider (Default)
Reads from `ArcSwap<DynamicConfig.auth>` per ADR-030. Every authorized key gets
a default scope set. No database dependency. This is the default for CLI and
single-node deployments.
```rust
pub struct ConfigIdentityProvider {
auth_config: Arc<ArcSwap<DynamicConfig>>,
}
impl IdentityProvider for ConfigIdentityProvider {
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
let config = self.auth_config.load();
config.auth.ssh.authorized_keys.get(fingerprint)
.map(|key_entry| Identity {
id: fingerprint.to_string(),
scopes: key_entry.scopes.clone(),
resources: key_entry.resources.clone(),
})
}
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
// Verify Ed25519 signature against the same authorized_keys set
// Resolve to the same Identity as SSH auth would produce
}
}
```
### StorageIdentityProvider (Production)
Implemented in `alknet-storage` (not in alknet-core). Backed by SQLite
`peer_credentials` and `api_keys` tables plus the ACL graph. Resolves
fingerprint → account → organization membership → effective scopes. Uses the
`IdentityProvider` trait defined in alknet-core, providing the concrete impl via
the trait.
### AuthProtocol irpc Service
The `AuthProtocol` irpc service (behind the `irpc` feature flag per ADR-028)
provides an async boundary for auth verification. It is one way to satisfy the
`IdentityProvider` trait, not a replacement for it:
```rust
enum AuthProtocol {
VerifyPubkey { fingerprint: String, key_data: Vec<u8> },
VerifyToken { token_bytes: Vec<u8>, timestamp: u64 },
ReloadKeys,
CheckAccess { identity: Identity, operation: String },
}
enum AuthResult {
Ok(Identity),
Denied(String),
}
```
The relationship:
- **Trait-based path**: Handler calls `identity_provider.resolve_from_fingerprint()`
directly. Zero overhead. Used when irpc is disabled or when the
implementation is local.
- **irpc path**: Handler calls `identity_provider.resolve_from_fingerprint()`,
which internally delegates to `AuthProtocol::VerifyPubkey` via an irpc client.
Used in production deployments with SQLite-backed auth.
Both paths produce the same `Identity` result.
### Auth Flows
**SSH key auth** (existing, unchanged):
```
Client connects → SSH handshake → auth_publickey() callback
→ IdentityProvider::resolve_from_fingerprint(fingerprint)
→ Some(Identity) or None
```
**Token auth** (new, for non-SSH transports):
```
Browser connects → WebTransport CONNECT request
→ Extract token from URL path or Authorization header
→ IdentityProvider::resolve_from_token(token)
→ Some(Identity) or None
```
Both paths produce an `Identity`. The `Identity` is attached to the connection
and used by `ForwardingPolicy` and call protocol for authorization decisions.
## Constraints
- `Identity` and `IdentityProvider` live in `alknet_core::auth`. No database
dependency at the core level (ADR-029).
- alknet-storage implements the core trait — the dependency goes from storage
to core, not the other way.
- The `id` field in `Identity` serves dual purpose (fingerprint or UUID). This
is a deliberate simplification — downstream consumers don't need to know the
source.
- Certificate authority tokens are not supported for token auth in v1 (ADR-023).
- The irpc feature flag means nodes that only do SSH tunneling don't need the
service layer overhead.
## Open Questions
- None specific to this spec. See [open-questions.md](open-questions.md) for
general auth questions (OQ-15, OQ-19).
## Design Decisions
| ADR | Decision | Summary |
|-----|----------|---------|
| [029](decisions/029-identity-core-type.md) | Identity as core type | `Identity` and `IdentityProvider` live in alknet-core, not storage |
| [028](decisions/028-auth-irpc-service.md) | Auth as irpc service | `AuthProtocol` behind feature flag; `IdentityProvider` is the contract |
| [023](decisions/023-unified-auth-shared-key-material.md) | Unified auth | Same key material for SSH and token auth; same `Identity` result |
## References
- [auth.md](auth.md) — Token authentication, AuthPolicy, WebTransport session handling
- [research/services.md](../research/services.md) — AuthService, AuthProtocol definition
- [research/integration-plan.md](../research/integration-plan.md) — Phase 1.2
- [ADR-030](decisions/030-static-dynamic-config-split.md) — DynamicConfig (ConfigIdentityProvider reads from it)
- [ADR-031](decisions/031-forwarding-policy.md) — ForwardingPolicy consumes Identity.scopes