tasks: decompose vault, core, call crates into 28 atomic implementation tasks
Break down the three initial crates (alknet-vault, alknet-core, alknet-call) into dependency-ordered task files for implementation agents. Structure: - tasks/vault/ (10 tasks) — drift fixes from ADR-025/026 refactor, review, spec sync. Vault is independent and can run fully in parallel with core/call. - tasks/core/ (6 tasks) — crate init, core types, config, auth, endpoint, review. Core is foundational; call depends on it. - tasks/call/ (12 tasks) — split into registry/ and protocol/ topic subdirs reflecting the two subsystems. CallAdapter is the merge point. Key decisions: - Drifts 3+9+10 grouped as one task (key-versioning-rotation) — the complete ADR-021 rotation feature that doesn't compile in pieces - Reviews injected at end of each crate phase (vault, core, call) - Vault spec-sync task removes the drift table and bumps doc status to stable - ACME deferred in core/endpoint (noted as TODO; X509 manual certs for now) - OperationEnv kept as a trait (load-bearing for ADR-024 layering) Validated: 28 tasks, no cycles, 11 generations of parallel work. Critical path runs through call (11 tasks). Vault completes by generation 4. 6 high-risk tasks identified (21%): irpc-removal, endpoint, operation-context, operation-env, call-adapter, abort-cascade.
This commit is contained in:
190
tasks/core/config.md
Normal file
190
tasks/core/config.md
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
id: core/config
|
||||
name: Implement StaticConfig, DynamicConfig, AuthPolicy, ApiKeyEntry, ConfigReloadHandle, TlsIdentity
|
||||
status: pending
|
||||
depends_on: [core/core-types]
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the configuration types in `src/config.rs`. These are the config
|
||||
structures consumed by the endpoint and the CLI binary. StaticConfig is
|
||||
immutable at startup; DynamicConfig is hot-reloadable via ArcSwap.
|
||||
|
||||
### StaticConfig
|
||||
|
||||
```rust
|
||||
pub struct StaticConfig {
|
||||
pub listen_addr: Option<SocketAddr>,
|
||||
pub tls_identity: Option<TlsIdentity>,
|
||||
pub iroh_relay: Option<RelayUrl>,
|
||||
pub drain_timeout: Duration,
|
||||
}
|
||||
```
|
||||
|
||||
Immutable configuration resolved at startup. `listen_addr` is None for
|
||||
iroh-only nodes. `tls_identity` is required if `listen_addr` is Some.
|
||||
|
||||
### TlsIdentity
|
||||
|
||||
```rust
|
||||
pub enum TlsIdentity {
|
||||
X509 { cert: PathBuf, key: PathBuf },
|
||||
RawKey(iroh::SecretKey),
|
||||
SelfSigned,
|
||||
}
|
||||
```
|
||||
|
||||
Three modes (OQ-12):
|
||||
- `X509`: domain certificate for browser/WebTransport clients
|
||||
- `RawKey`: RFC 7250 raw Ed25519 public key — default for P2P, no domain/CA
|
||||
- `SelfSigned`: development only
|
||||
|
||||
`RawKey` uses `iroh::SecretKey` (Ed25519) — re-exported from iroh, which
|
||||
alknet-core depends on (feature-gated). The key can be derived from
|
||||
alknet-vault at the assembly layer or generated fresh.
|
||||
|
||||
### DynamicConfig
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DynamicConfig {
|
||||
pub auth: AuthPolicy,
|
||||
pub rate_limits: RateLimitConfig,
|
||||
}
|
||||
```
|
||||
|
||||
Runtime-reloadable via ArcSwap.
|
||||
|
||||
### AuthPolicy
|
||||
|
||||
```rust
|
||||
pub struct AuthPolicy {
|
||||
pub authorized_fingerprints: HashSet<String>,
|
||||
pub api_keys: Vec<ApiKeyEntry>,
|
||||
}
|
||||
```
|
||||
|
||||
Fingerprints stored as strings (no russh dependency in core — ADR-003).
|
||||
Certificate authority entries deferred to alknet-ssh (omitted from v1 to avoid
|
||||
referencing an undefined type; adding back is additive).
|
||||
|
||||
### ApiKeyEntry
|
||||
|
||||
```rust
|
||||
pub struct ApiKeyEntry {
|
||||
pub prefix: String,
|
||||
pub hash: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub description: String,
|
||||
pub expires_at: Option<u64>,
|
||||
}
|
||||
```
|
||||
|
||||
Carries forward from reference implementation. Prefix (first 8 chars) for O(1)
|
||||
lookup, SHA-256 hash for verification.
|
||||
|
||||
### RateLimitConfig
|
||||
|
||||
```rust
|
||||
pub struct RateLimitConfig {
|
||||
pub max_connections_per_ip: usize,
|
||||
pub max_auth_attempts: usize,
|
||||
}
|
||||
```
|
||||
|
||||
### ArcSwap pattern
|
||||
|
||||
```rust
|
||||
let dynamic = Arc::new(ArcSwap::new(Arc::new(DynamicConfig::default())));
|
||||
```
|
||||
|
||||
- Reads: `dynamic.load()` returns `Arc<DynamicConfig>` — lock-free
|
||||
- Writes: `dynamic.store(Arc::new(new_config))` — atomic swap
|
||||
- No locks: ArcSwap uses atomic operations
|
||||
|
||||
### ConfigReloadHandle
|
||||
|
||||
```rust
|
||||
pub struct ConfigReloadHandle {
|
||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||
}
|
||||
|
||||
impl ConfigReloadHandle {
|
||||
pub fn reload(&self, new_config: DynamicConfig);
|
||||
pub fn dynamic(&self) -> Arc<DynamicConfig>;
|
||||
}
|
||||
```
|
||||
|
||||
- `reload()`: atomically replaces the dynamic config
|
||||
- `dynamic()`: returns current config as `Arc<DynamicConfig>`
|
||||
|
||||
**Config reload is a privilege-escalation path.** A reload that adds an
|
||||
authorized fingerprint or API key grants access immediately. The reload
|
||||
trigger must be authenticated/local-only (SIGHUP, file watch, or admin call
|
||||
protocol operation). The implementation must not ship a reload endpoint with
|
||||
no auth "for convenience."
|
||||
|
||||
### ConfigError
|
||||
|
||||
```rust
|
||||
pub enum ConfigError {
|
||||
InvalidFlag { name: String },
|
||||
KeyFileNotFound { path: String },
|
||||
BindFailed(io::Error),
|
||||
TlsConfig(io::Error),
|
||||
IncompatibleOptions,
|
||||
}
|
||||
```
|
||||
|
||||
### Defaults
|
||||
|
||||
- `drain_timeout`: 2 seconds
|
||||
- `max_connections_per_ip`: implementation default (reference uses a reasonable value)
|
||||
- `max_auth_attempts`: implementation default
|
||||
- `DynamicConfig::default()`: empty auth policy, default rate limits
|
||||
|
||||
### What NOT to include
|
||||
|
||||
Per the spec, StaticConfig does NOT include: `host_key`, `host_key_algorithm`,
|
||||
`proxy_config`, `stealth`, `transport_mode`, `listeners`. These are removed in
|
||||
the new model (ALPN dispatch replaces them — see config.md Key Differences).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `StaticConfig` struct with all fields per config.md
|
||||
- [ ] `TlsIdentity` enum with X509, RawKey, SelfSigned variants
|
||||
- [ ] `DynamicConfig` struct with `auth` and `rate_limits` fields
|
||||
- [ ] `AuthPolicy` struct with `authorized_fingerprints` and `api_keys`
|
||||
- [ ] `ApiKeyEntry` struct with all 5 fields
|
||||
- [ ] `RateLimitConfig` struct with both fields
|
||||
- [ ] `ConfigReloadHandle` with `reload()` and `dynamic()` methods
|
||||
- [ ] `ConfigError` enum with all variants
|
||||
- [ ] `DynamicConfig` derives `Clone`, `Debug` (for ArcSwap)
|
||||
- [ ] Default values match config.md (drain_timeout = 2s, etc.)
|
||||
- [ ] No russh dependency (fingerprints as strings)
|
||||
- [ ] Unit tests for Default impls
|
||||
- [ ] Unit test: ConfigReloadHandle reload swaps config atomically
|
||||
- [ ] `cargo test -p alknet-core` succeeds
|
||||
- [ ] `cargo clippy -p alknet-core` succeeds with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/core/config.md — all type definitions
|
||||
- docs/architecture/decisions/003-crate-decomposition.md — ADR-003 (no russh in core)
|
||||
- docs/architecture/decisions/010-alpn-router-and-endpoint.md — ADR-010 (no ListenerConfig)
|
||||
|
||||
## Notes
|
||||
|
||||
> Config reload is a privilege-escalation path — do not ship an unauthenticated
|
||||
> reload endpoint. The ArcSwap pattern carries forward from the reference
|
||||
> implementation. StaticConfig removes all SSH-centric fields (host_key,
|
||||
> stealth, transport_mode, listeners) — those are handler concerns now.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user