| id | ADR-014 | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| title | Sealed kernel traits with explicit adapter registration | |||||||||||||
| status | accepted | |||||||||||||
| date | 2026-04-14 | |||||||||||||
| phase | Phase 2 (first non-mock impl) | |||||||||||||
| deciders |
|
|||||||||||||
| tags |
|
|||||||||||||
| affects_crates |
|
|||||||||||||
| affects_layers |
|
|||||||||||||
| supersedes | ||||||||||||||
| superseded_by | ||||||||||||||
| related |
|
|||||||||||||
| requires |
|
|||||||||||||
| enables | ||||||||||||||
| amends | ||||||||||||||
| fci |
|
|||||||||||||
| inv | ||||||||||||||
| shadow_zones | ||||||||||||||
| nika_codes | ||||||||||||||
| timeline | v0.80.0, Phase 2 -- sealing before first L1 admission | |||||||||||||
| follow_ups |
|
nika-kernel (L0.5) defines ~20 public async traits — Provider, MemoryStore, ToolExecutor, FsRead, HttpGet, ShellExecutor, etc. Today only nika-kernel-mock implements them (L0.5 test). As Phase 2 admits L1 effect crates (nika-http, nika-fs, nika-process), each trait gains real implementors.
The open question: can an external crate (outside supernovae-st/nika workspace) implement these traits? If yes, every method signature becomes load-bearing across semver — adding a default-method parameter is a breaking change for external implementors. If no, we preserve flexibility to evolve method signatures as we learn.
2026 Rust practice (axum, serde, uv) uses the sealed-trait pattern (pub trait Foo: private::Sealed { ... }) to prevent external impls while keeping the trait public for users who depend on it via dyn Foo. rust-analyzer, rustls, tokio all seal core traits.
All public traits in nika-kernel are sealed via the private::Sealed supertrait pattern:
// nika-kernel/src/lib.rs
mod private {
pub trait Sealed {}
}
// nika-kernel/src/provider.rs
pub trait ProviderInfer: private::Sealed + Send + Sync { /* ... */ }
// nika-kernel/src/provider.rs (internal, for blanket Sealed impls)
impl<T: crate::Provider + ?Sized> private::Sealed for T {}External crates cannot impl ProviderInfer for MyType (compile error: Sealed not implemented, and Sealed is private). Instead, external integrations route through:
- Adapter macros (
nika_kernel::provider_impl!) for workspace-local crates (nika-provider-rig,nika-provider-native,nika-provider-mock). nika-builtin-*wrappers for ecosystem integrations (GitHub, Cloud, Workspace — ADR tbd).- Future pck packages (plugin protocol, Phase 4+) for community extensions.
Adding a trait method still requires care, but the set of implementors is finite and controlled by the workspace.
- Method signatures and default methods can evolve within v0.x without breaking external code (there isn't any external impl by construction).
- Clear path for ecosystem extensions (adapter macros > plugin protocol > pck) — enforced architecturally.
- Matches Rust 2026 canonical pattern.
- Some friction for hypothetical out-of-workspace adapters — but there are none today, and when they emerge they belong as
nika-builtin-*or pck packages per ADR-004's crate-count discipline. - Adds a
private::Sealedmodule + blanket impl per trait group.
- Language-level
sealedkeyword RFC is still unmerged as of 2026-Q1 — the supertrait pattern is idiomatic.
- Pattern sources: axum, serde (
private::Sealed), rustls (server::ServerConfigsealed), tokio-stream crates/nika-kernel/src/lib.rs:19-34— trait hierarchy (to be updated with sealed pattern)
Rejected — every method change becomes potential breakage.
Rejected — loses dyn Trait + breaks test substitutability (kernel-mock).
Not valid in Rust — #[non_exhaustive] is structs/enums only.
- ADR-006 — kernel ISP traits (which this seals)
- ADR-012 — typestate runtime (uses sealed kernel via
bind_kernel(&dyn Kernel))
Migration: sealing can be added to nika-kernel in a single commit before any L1 effect crate admits. No breaking change to nika-kernel-mock (workspace-local, blanket-Sealed applies). Plan for Phase 2 Session 1.