Skip to content

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:

TypeNamePurposeIsolationSize
NPSNip SensorTelemetry agentsDirect MMIOKB
NPLNip LibraryDrivers, filtersOne hop from kernel (9P)KB-MB
NPXNip eXtensionHot-loadable hooks (eBPF replacement)Trap dispatchKB
NPMNip ModulePure logic librariesNo hardware accessKB-MB
NPKNip PackageEnd-user applicationsFull sandboxMB
NPINip InterfaceAPI bridges, service discovery/Bus/ symlinkKB

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

OptionWhy Not
Docker containers for everythingOne-size-fits-all wastes memory; 200MB per container on MCU is absurd
Debian .deb for everythingNo isolation, shared /usr pollutes global namespace
Flatpak/SnapEach package bundles own libc; 200MB per desktop app
Static binary onlyWorks 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

ComponentPhaseSource
TCP/IPGraftLwIP (BSD)
ShellGraftmksh (MirBSD)
Filesystem (NexFS)SovereignNative Zig
Transport (UTCP)SovereignNative Zig
Package Manager (nip)SovereignNative Nim
Build ToolkitSovereignNative Nim
Kernel (Rumpk)SovereignNative Nim + Zig
Init/BootSovereignNative

Alternatives Rejected

OptionWhy Not
All from scratch10x development time, high bug risk, reinventing solved problems
All from upstreamForever dependent, can't optimize for Nexus, can't remove POSIX bloat
Fork and divergeUnmaintainable; 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

OptionWhy Not
Traditional FHSapt 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 monolithsWorks 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/app need 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:

kdl
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

OptionWhy Not
YAMLWhitespace-significant parsing is fragile; !! escape sequences are cryptic
TOMLFlat structure makes nested configs awkward; table syntax is verbose
JSONNo comments, verbose syntax, no types beyond string/number/bool
HCL (Terraform)JSON-like verbosity; Terraform ecosystem coupling
DhallTuring-complete (enables arbitrary computation, reduces predictability)
Nix languagePowerful 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)