Packaging Decisions
Four decisions that make Nexus packaging unlike anything in the Linux/BSD world.
P1: Six Package Types
Status: Accepted
Context
Docker uses one model (container) for everything — databases, web servers, CLI tools, drivers. Debian uses one model (.deb) with global /usr dependencies. Neither accounts for the vast difference between a temperature sensor driver (10KB, direct MMIO) and a web browser (500MB, full sandbox).
Decision
Six package archetypes, each optimized for its role:
| Type | Name | Purpose | Isolation | Size |
|---|---|---|---|---|
| NPS | Nip Sensor | Telemetry agents | Direct MMIO | KB |
| NPL | Nip Library | Drivers, filters | One hop from kernel (9P) | KB-MB |
| NPX | Nip eXtension | Hot-loadable hooks (eBPF replacement) | Trap dispatch | KB |
| NPM | Nip Module | Pure logic libraries | No hardware access | KB-MB |
| NPK | Nip Package | End-user applications | Full sandbox | MB |
| NPI | Nip Interface | API bridges, service discovery | /Bus/ symlink | KB |
Each type has different:
- Startup time (NPS: µs, NPK: ms)
- Memory overhead (NPS: none, NPK: full Membrane)
- Security boundary (NPS: PMP only, NPK: pledge + capability + cell)
- Build pipeline (NPS: bare binary, NPK: full quarantine pipeline)
Alternatives Rejected
| Option | Why Not |
|---|---|
| Docker containers for everything | One-size-fits-all wastes memory; 200MB per container on MCU is absurd |
| Debian .deb for everything | No isolation, shared /usr pollutes global namespace |
| Flatpak/Snap | Each package bundles own libc; 200MB per desktop app |
| Static binary only | Works but no code sharing, harder to patch shared library CVEs |
Consequences
- Drivers (NPL) don't need full POSIX stack (saves 50MB per driver)
- Sensors (NPS) start in microseconds (no libc initialization)
- Clear intent: package type communicates isolation requirements at a glance
- Isolation tuned per type (NPL may share kernel memory; NPK gets full sandbox)
- Developers must choose correct type (higher initial learning curve)
- Migration between types requires repackaging
P2: Graft → Evolve → Sovereignize
Status: Accepted
Context
Building everything from scratch takes years and produces immature code. Forking upstream creates divergent codebases that can't merge. The Linux and BSD ecosystems contain decades of battle-tested code. The question isn't "build vs. buy" — it's "borrow now, replace later."
Decision
Three-phase component lifecycle:
Phase 1: Graft
Use upstream artifacts as-is. No modifications.
Example: LwIP TCP/IP stack from git submodule. BSD-licensed. Works.
Phase 2: Evolve
Replace components incrementally where Nexus needs differ from upstream.
Example: Standard TCP for Internet traffic, UTCP wrapper for cluster traffic. Both coexist.
Phase 3: Sovereignize
Full native reimplementation. Zero external dependencies.
Example: Native Nim TCP stack replacing LwIP (post-MVP, when edge cases are understood).
Current State
| Component | Phase | Source |
|---|---|---|
| TCP/IP | Graft | LwIP (BSD) |
| Shell | Graft | mksh (MirBSD) |
| Filesystem (NexFS) | Sovereign | Native Zig |
| Transport (UTCP) | Sovereign | Native Zig |
| Package Manager (nip) | Sovereign | Native Nim |
| Build Toolkit | Sovereign | Native Nim |
| Kernel (Rumpk) | Sovereign | Native Nim + Zig |
| Init/Boot | Sovereign | Native |
Alternatives Rejected
| Option | Why Not |
|---|---|
| All from scratch | 10x development time, high bug risk, reinventing solved problems |
| All from upstream | Forever dependent, can't optimize for Nexus, can't remove POSIX bloat |
| Fork and diverge | Unmaintainable; merge conflicts with upstream destroy velocity |
Consequences
- Rapid MVP using proven code (LwIP, mksh work today)
- Gradual technical debt payoff (replace when you understand the problem)
- Can optimize critical path first (Evolve only hot components)
- Intermediate versions have mixed code quality
- Grafted code must not leak into architecture (clean boundaries required)
P3: GoboLinux Hierarchy
Status: Accepted
Context
The Filesystem Hierarchy Standard (FHS) puts all binaries in /usr/bin, all libraries in /usr/lib. One broken package update can corrupt shared libraries system-wide. Nix solves this with /nix/store/hash-name/ but the paths are cryptic and undiscoverable.
Decision
Content-addressed per-version directories, inspired by GoboLinux:
/Cas/blake3:a1b2c3.../
bin/
lib/
share/
/Cell/App/firefox/
fs/ → /Cas/blake3:a1b2c3.../ (symlink to active version)- Each package version is content-addressed (hash includes all transitive dependencies)
- Old versions remain in
/Cas/— instant rollback by relinking - Multiple versions coexist (Python 3.9 and 3.10, same system, zero conflict)
- Deduplication at block level via NexFS (identical binaries share storage)
Alternatives Rejected
| Option | Why Not |
|---|---|
| Traditional FHS | apt upgrade breaks fragile dependency chains; shared /usr is a liability |
| Nix store | /nix/store/hash-name/ is awkward, non-discoverable, hard to debug |
| Statically linked monoliths | Works but code duplication; can't patch shared library CVEs efficiently |
| Containers (Docker volumes) | Opaque layers, hard to inspect, storage inefficient |
Consequences
- Downgrades are atomic (relink one symlink)
- Multiple versions coexist without conflict
- NexFS deduplication means shared binaries stored once on disk
- Apps must not hardcode absolute paths (use relative or
/Cell/paths) - Legacy tools expecting
/usr/bin/appneed Membrane path translation
P4: KDL Over YAML and TOML
Status: Accepted
Context
YAML is whitespace-significant (a tab vs. spaces can break your deployment). TOML is flat (nested configurations become awkward). JSON has no comments. HCL (Terraform) is verbose. Nexus needs a configuration language for package manifests, cell blueprints, build recipes, and system policies — all deeply nested, all declarative.
Decision
KDL (KDL Document Language) for all configuration:
graft "networking" {
origin {
system "alpine"
package "networkmanager"
version "1.44.0"
}
pledge {
allow "net-admin"
deny "ptrace"
}
}
cell "web-server" {
cores 2
memory "512MB"
pledge "net" "stdio" "rw"
mount "/data" from="/Cas/blake3:..." readonly=true
}- S-expression inspired, whitespace-insensitive
- Comments with
//and/* */ - Not Turing-complete (pure declaration, no computation)
- KDL compiler targets JSON, CBOR, and binary formats
Alternatives Rejected
| Option | Why Not |
|---|---|
| YAML | Whitespace-significant parsing is fragile; !! escape sequences are cryptic |
| TOML | Flat structure makes nested configs awkward; table syntax is verbose |
| JSON | No comments, verbose syntax, no types beyond string/number/bool |
| HCL (Terraform) | JSON-like verbosity; Terraform ecosystem coupling |
| Dhall | Turing-complete (enables arbitrary computation, reduces predictability) |
| Nix language | Powerful but complex; Nix's learning curve is legendary |
Consequences
- Declarative syntax prevents Turing-complete escape (policies are predictable)
- Nested structure maps naturally to object hierarchy (cells contain mounts contain paths)
- Comments are first-class (unlike JSON)
- Not Turing-complete means no conditional logic in configs (this is intentional)
- KDL is a niche language (limited tooling, IDE support developing)
- Developers must learn new syntax (though simpler than most alternatives)