770 lines
18 KiB
Markdown
770 lines
18 KiB
Markdown
# Nippels Developer Guide
|
|
|
|
**Complete guide for developers working with Nippels (NimPak Cells)**
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Architecture Overview](#architecture-overview)
|
|
2. [Core Components](#core-components)
|
|
3. [API Reference](#api-reference)
|
|
4. [Extension Points](#extension-points)
|
|
5. [Development Setup](#development-setup)
|
|
6. [Testing](#testing)
|
|
7. [Contributing](#contributing)
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
### High-Level Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Nippel Manager │
|
|
│ (nippels.nim - Orchestration & Public API) │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Profile │ │ Namespace │ │ XDG │ │
|
|
│ │ Manager │ │ Subsystem │ │ Enforcer │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ CAS │ │ Merkle │ │ UTCP │ │
|
|
│ │ Backend │ │ Tree │ │ Protocol │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Nexter │ │ Decentralized│ │
|
|
│ │ Comm │ │ Architecture│ │
|
|
│ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Module Dependencies
|
|
|
|
```
|
|
nippels.nim (main orchestrator)
|
|
├── nippel_types.nim (shared types)
|
|
├── profile_manager.nim (security profiles)
|
|
├── namespace_subsystem.nim (Linux namespaces)
|
|
├── xdg_enforcer.nim (XDG directory enforcement)
|
|
├── cas_backend.nim (content-addressable storage)
|
|
├── merkle_tree.nim (integrity verification)
|
|
├── utcp_protocol.nim (AI-addressability)
|
|
├── nexter_comm.nim (Nexter communication)
|
|
└── decentralized.nim (P2P features)
|
|
```
|
|
|
|
|
|
## Core Components
|
|
|
|
### 1. Nippel Manager (nippels.nim)
|
|
|
|
**Purpose:** Main orchestration and public API
|
|
|
|
**Key Types:**
|
|
```nim
|
|
type
|
|
NippelManager* = ref object
|
|
cells*: Table[string, Nippel]
|
|
profileManager*: ProfileManager
|
|
activeCells*: HashSet[string]
|
|
merkleTrees*: Table[string, MerkleTree]
|
|
|
|
Nippel* = object
|
|
name*: string
|
|
profile*: SecurityProfile
|
|
settings*: ProfileSettings
|
|
rootPath*: string
|
|
state*: NippelState
|
|
namespaceHandle*: Option[NamespaceHandle]
|
|
merkleRoot*: string
|
|
utcpAddress*: UTCPAddress
|
|
```
|
|
|
|
**Key Functions:**
|
|
```nim
|
|
proc createNippel*(manager: NippelManager, name: string,
|
|
profile: SecurityProfile): Result[Nippel, NippelError]
|
|
|
|
proc activateNippel*(manager: NippelManager, name: string): Result[void, NippelError]
|
|
|
|
proc deactivateNippel*(manager: NippelManager, name: string): Result[void, NippelError]
|
|
|
|
proc removeNippel*(manager: NippelManager, name: string,
|
|
purge: bool = false): Result[void, NippelError]
|
|
```
|
|
|
|
### 2. Profile Manager (profile_manager.nim)
|
|
|
|
**Purpose:** Security profile management
|
|
|
|
**Key Types:**
|
|
```nim
|
|
type
|
|
SecurityProfile* = enum
|
|
Workstation, Homestation, Satellite, NetworkIOT, Server
|
|
|
|
ProfileSettings* = object
|
|
isolationLevel*: IsolationLevel
|
|
desktopIntegration*: bool
|
|
networkAccess*: NetworkAccess
|
|
resourceLimits*: ResourceLimits
|
|
xdgStrategy*: XDGStrategy
|
|
|
|
ProfileOverrides* = object
|
|
isolationLevel*: Option[IsolationLevel]
|
|
desktopIntegration*: Option[bool]
|
|
networkAccess*: Option[NetworkAccess]
|
|
```
|
|
|
|
**Key Functions:**
|
|
```nim
|
|
proc loadProfile*(manager: ProfileManager,
|
|
profile: SecurityProfile): ProfileSettings
|
|
|
|
proc applyOverrides*(settings: ProfileSettings,
|
|
overrides: ProfileOverrides): ProfileSettings
|
|
```
|
|
|
|
### 3. Namespace Subsystem (namespace_subsystem.nim)
|
|
|
|
**Purpose:** Linux namespace management
|
|
|
|
**Key Types:**
|
|
```nim
|
|
type
|
|
IsolationLevel* = enum
|
|
None, Standard, Strict, Quantum
|
|
|
|
NamespaceConfig* = object
|
|
mountNS*: bool
|
|
pidNS*: bool
|
|
ipcNS*: bool
|
|
networkNS*: bool
|
|
utsNS*: bool
|
|
userNS*: bool
|
|
|
|
NamespaceHandle* = object
|
|
pid*: int
|
|
namespaces*: NamespaceConfig
|
|
```
|
|
|
|
**Key Functions:**
|
|
```nim
|
|
proc createNamespaces*(config: NamespaceConfig): Result[NamespaceHandle, string]
|
|
|
|
proc enterNamespace*(handle: NamespaceHandle): Result[void, string]
|
|
|
|
proc exitNamespace*(handle: NamespaceHandle): Result[void, string]
|
|
|
|
proc destroyNamespaces*(handle: NamespaceHandle): Result[void, string]
|
|
```
|
|
|
|
### 4. XDG Enforcer (xdg_enforcer.nim)
|
|
|
|
**Purpose:** XDG directory enforcement
|
|
|
|
**Key Types:**
|
|
```nim
|
|
type
|
|
XDGStrategy* = enum
|
|
Portable, SystemIntegrated
|
|
|
|
XDGDirectories* = object
|
|
dataHome*: string
|
|
configHome*: string
|
|
cacheHome*: string
|
|
stateHome*: string
|
|
```
|
|
|
|
**Key Functions:**
|
|
```nim
|
|
proc createXDGStructure*(rootPath: string,
|
|
strategy: XDGStrategy): Result[XDGDirectories, string]
|
|
|
|
proc setXDGEnvironment*(dirs: XDGDirectories): Result[void, string]
|
|
|
|
proc redirectLegacyPaths*(dirs: XDGDirectories): Result[void, string]
|
|
```
|
|
|
|
### 5. CAS Backend (cas_backend.nim)
|
|
|
|
**Purpose:** Content-addressable storage with deduplication
|
|
|
|
**Key Types:**
|
|
```nim
|
|
type
|
|
CASBackend* = ref object
|
|
storePath*: string
|
|
hashAlgorithm*: HashAlgorithm
|
|
refCounts*: Table[string, int]
|
|
|
|
HashAlgorithm* = enum
|
|
Xxh3_128, Blake2b512
|
|
```
|
|
|
|
**Key Functions:**
|
|
```nim
|
|
proc storeFile*(cas: CASBackend, filePath: string): Result[string, string]
|
|
|
|
proc retrieveFile*(cas: CASBackend, hash: string,
|
|
destPath: string): Result[void, string]
|
|
|
|
proc garbageCollect*(cas: CASBackend): Result[int, string]
|
|
```
|
|
|
|
### 6. Merkle Tree (merkle_tree.nim)
|
|
|
|
**Purpose:** Integrity verification
|
|
|
|
**Key Types:**
|
|
```nim
|
|
type
|
|
MerkleTree* = ref object
|
|
root*: MerkleNode
|
|
algorithm*: HashAlgorithm
|
|
|
|
MerkleNode* = ref object
|
|
hash*: string
|
|
path*: string
|
|
children*: seq[MerkleNode]
|
|
```
|
|
|
|
**Key Functions:**
|
|
```nim
|
|
proc buildTreeFromFiles*(files: seq[string],
|
|
algorithm: HashAlgorithm): Result[MerkleTree, string]
|
|
|
|
proc verifyTree*(tree: MerkleTree): Result[bool, string]
|
|
|
|
proc updateTree*(tree: MerkleTree,
|
|
changes: seq[FileChange]): Result[MerkleTree, string]
|
|
|
|
proc diffTrees*(tree1, tree2: MerkleTree): seq[FileDiff]
|
|
```
|
|
|
|
### 7. UTCP Protocol (utcp_protocol.nim)
|
|
|
|
**Purpose:** AI-addressability
|
|
|
|
**Key Types:**
|
|
```nim
|
|
type
|
|
UTCPAddress* = object
|
|
scheme*: string
|
|
host*: string
|
|
resource*: string
|
|
port*: int
|
|
|
|
UTCPRequest* = object
|
|
address*: UTCPAddress
|
|
method*: string
|
|
headers*: Table[string, string]
|
|
body*: string
|
|
|
|
UTCPResponse* = object
|
|
status*: int
|
|
headers*: Table[string, string]
|
|
body*: string
|
|
```
|
|
|
|
**Key Functions:**
|
|
```nim
|
|
proc assignUTCPAddress*(nippelName: string): UTCPAddress
|
|
|
|
proc handleUTCPRequest*(request: UTCPRequest): Result[UTCPResponse, string]
|
|
|
|
proc formatUTCPAddress*(address: UTCPAddress): string
|
|
```
|
|
|
|
|
|
## API Reference
|
|
|
|
### Creating a Nippel
|
|
|
|
```nim
|
|
import nippels, nippel_types, profile_manager
|
|
|
|
# Create manager
|
|
let manager = newNippelManager()
|
|
|
|
# Create Nippel with default profile
|
|
let result = manager.createNippel("my-app", Workstation)
|
|
if result.isOk:
|
|
echo "Nippel created successfully"
|
|
else:
|
|
echo "Error: ", result.error
|
|
|
|
# Create with custom overrides
|
|
let overrides = ProfileOverrides(
|
|
isolationLevel: some(Strict),
|
|
networkAccess: some(Limited)
|
|
)
|
|
let result2 = manager.createNippelWithOverrides("secure-app", Satellite, overrides)
|
|
```
|
|
|
|
### Activating a Nippel
|
|
|
|
```nim
|
|
# Activate Nippel
|
|
let activateResult = manager.activateNippel("my-app")
|
|
if activateResult.isOk:
|
|
echo "Nippel activated"
|
|
# Now in isolated environment
|
|
else:
|
|
echo "Activation failed: ", activateResult.error
|
|
|
|
# Check if active
|
|
if manager.isNippelActive("my-app"):
|
|
echo "Nippel is active"
|
|
|
|
# Deactivate
|
|
let deactivateResult = manager.deactivateNippel("my-app")
|
|
```
|
|
|
|
### Working with Profiles
|
|
|
|
```nim
|
|
import profile_manager
|
|
|
|
let profileMgr = newProfileManager()
|
|
|
|
# Load profile
|
|
let settings = profileMgr.loadProfile(Workstation)
|
|
echo "Isolation level: ", settings.isolationLevel
|
|
echo "Desktop integration: ", settings.desktopIntegration
|
|
|
|
# Apply overrides
|
|
let overrides = ProfileOverrides(
|
|
desktopIntegration: some(false)
|
|
)
|
|
let customSettings = settings.applyOverrides(overrides)
|
|
```
|
|
|
|
### Using CAS Backend
|
|
|
|
```nim
|
|
import cas_backend
|
|
|
|
let cas = newCASBackend("/var/lib/nip/cas", Xxh3_128)
|
|
|
|
# Store file
|
|
let storeResult = cas.storeFile("/path/to/file.txt")
|
|
if storeResult.isOk:
|
|
let hash = storeResult.value
|
|
echo "File stored with hash: ", hash
|
|
|
|
# Retrieve file
|
|
let retrieveResult = cas.retrieveFile(hash, "/path/to/dest.txt")
|
|
|
|
# Garbage collect
|
|
let gcResult = cas.garbageCollect()
|
|
if gcResult.isOk:
|
|
echo "Removed ", gcResult.value, " unreferenced entries"
|
|
```
|
|
|
|
### Building Merkle Trees
|
|
|
|
```nim
|
|
import merkle_tree
|
|
|
|
# Build tree from files
|
|
let files = @["/path/to/file1.txt", "/path/to/file2.txt"]
|
|
let treeResult = buildTreeFromFiles(files, Xxh3_128)
|
|
|
|
if treeResult.isOk:
|
|
let tree = treeResult.value
|
|
echo "Merkle root: ", tree.root.hash
|
|
|
|
# Verify tree
|
|
let verifyResult = tree.verifyTree()
|
|
if verifyResult.isOk and verifyResult.value:
|
|
echo "Tree verified successfully"
|
|
```
|
|
|
|
### UTCP Protocol
|
|
|
|
```nim
|
|
import utcp_protocol
|
|
|
|
# Assign UTCP address
|
|
let address = assignUTCPAddress("my-app")
|
|
echo "UTCP address: ", formatUTCPAddress(address)
|
|
# Output: utcp://localhost/nippel/my-app
|
|
|
|
# Create request
|
|
let request = UTCPRequest(
|
|
address: address,
|
|
method: "GET",
|
|
headers: {"Accept": "application/json"}.toTable,
|
|
body: ""
|
|
)
|
|
|
|
# Handle request
|
|
let response = handleUTCPRequest(request)
|
|
if response.isOk:
|
|
echo "Response: ", response.value.body
|
|
```
|
|
|
|
## Extension Points
|
|
|
|
### Custom Security Profiles
|
|
|
|
You can define custom security profiles:
|
|
|
|
```nim
|
|
# In profile_manager.nim
|
|
|
|
proc loadCustomProfile*(name: string): ProfileSettings =
|
|
case name
|
|
of "my-custom-profile":
|
|
result = ProfileSettings(
|
|
isolationLevel: Strict,
|
|
desktopIntegration: true,
|
|
networkAccess: Limited,
|
|
resourceLimits: ResourceLimits(
|
|
maxMemory: 2_000_000_000, # 2GB
|
|
maxCPU: 50
|
|
),
|
|
xdgStrategy: Portable
|
|
)
|
|
else:
|
|
raise newException(ValueError, "Unknown profile: " & name)
|
|
```
|
|
|
|
### Custom Hash Algorithms
|
|
|
|
Add support for new hash algorithms:
|
|
|
|
```nim
|
|
# In cas_backend.nim
|
|
|
|
type
|
|
HashAlgorithm* = enum
|
|
Xxh3_128, Blake2b512, MyCustomHash
|
|
|
|
proc computeHash*(data: string, algorithm: HashAlgorithm): string =
|
|
case algorithm
|
|
of Xxh3_128:
|
|
result = xxh3_128(data)
|
|
of Blake2b512:
|
|
result = blake2b_512(data)
|
|
of MyCustomHash:
|
|
result = myCustomHashFunction(data)
|
|
```
|
|
|
|
### Custom UTCP Methods
|
|
|
|
Extend UTCP protocol with custom methods:
|
|
|
|
```nim
|
|
# In utcp_protocol.nim
|
|
|
|
proc handleCustomMethod*(request: UTCPRequest): Result[UTCPResponse, string] =
|
|
case request.method
|
|
of "CUSTOM_METHOD":
|
|
# Handle custom method
|
|
let response = UTCPResponse(
|
|
status: 200,
|
|
headers: {"Content-Type": "application/json"}.toTable,
|
|
body: """{"result": "success"}"""
|
|
)
|
|
return ok(response)
|
|
else:
|
|
return err("Unknown method: " & request.method)
|
|
```
|
|
|
|
### Custom Namespace Configurations
|
|
|
|
Define custom namespace configurations:
|
|
|
|
```nim
|
|
# In namespace_subsystem.nim
|
|
|
|
proc getCustomNamespaceConfig*(level: string): NamespaceConfig =
|
|
case level
|
|
of "my-custom-level":
|
|
result = NamespaceConfig(
|
|
mountNS: true,
|
|
pidNS: true,
|
|
ipcNS: true,
|
|
networkNS: false, # Custom: no network isolation
|
|
utsNS: true,
|
|
userNS: false
|
|
)
|
|
else:
|
|
raise newException(ValueError, "Unknown isolation level: " & level)
|
|
```
|
|
|
|
|
|
## Development Setup
|
|
|
|
### Prerequisites
|
|
|
|
- Nim 2.0.0 or later
|
|
- Linux kernel with namespace support
|
|
- xxHash library (`nimble install xxhash`)
|
|
- Standard Nim libraries
|
|
|
|
### Building from Source
|
|
|
|
```bash
|
|
# Clone repository
|
|
git clone https://github.com/nexusos/nip.git
|
|
cd nip
|
|
|
|
# Install dependencies
|
|
nimble install -d
|
|
|
|
# Build
|
|
nimble build
|
|
|
|
# Run tests
|
|
nimble test
|
|
|
|
# Install
|
|
nimble install
|
|
```
|
|
|
|
### Development Environment
|
|
|
|
```bash
|
|
# Set up development environment
|
|
export NIP_DEV_MODE=1
|
|
export NIP_LOG_LEVEL=debug
|
|
|
|
# Build with debug symbols
|
|
nim c -d:debug --debugger:native nip/src/nimpak/nippels.nim
|
|
|
|
# Run with debugger
|
|
gdb --args ./nippels
|
|
```
|
|
|
|
### Code Style
|
|
|
|
Follow Nim standard style guide:
|
|
|
|
```nim
|
|
# Good
|
|
proc createNippel*(manager: NippelManager, name: string): Result[Nippel, NippelError] =
|
|
## Creates a new Nippel with the given name
|
|
if name.len == 0:
|
|
return err(NippelError(code: InvalidName, message: "Name cannot be empty"))
|
|
|
|
# Implementation
|
|
ok(nippel)
|
|
|
|
# Bad
|
|
proc CreateNippel(manager:NippelManager,name:string):Result[Nippel,NippelError]=
|
|
if name.len==0:return err(NippelError(code:InvalidName,message:"Name cannot be empty"))
|
|
ok(nippel)
|
|
```
|
|
|
|
### Documentation Standards
|
|
|
|
All public APIs must be documented:
|
|
|
|
```nim
|
|
proc createNippel*(manager: NippelManager, name: string,
|
|
profile: SecurityProfile): Result[Nippel, NippelError] =
|
|
## Creates a new Nippel with the specified security profile.
|
|
##
|
|
## Parameters:
|
|
## - manager: The NippelManager instance
|
|
## - name: Unique name for the Nippel
|
|
## - profile: Security profile to use
|
|
##
|
|
## Returns:
|
|
## - Ok(Nippel) on success
|
|
## - Err(NippelError) on failure
|
|
##
|
|
## Example:
|
|
## ```nim
|
|
## let manager = newNippelManager()
|
|
## let result = manager.createNippel("my-app", Workstation)
|
|
## if result.isOk:
|
|
## echo "Created: ", result.value.name
|
|
## ```
|
|
|
|
# Implementation
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Unit Tests
|
|
|
|
```nim
|
|
# test_nippels.nim
|
|
import unittest, nippels, nippel_types
|
|
|
|
suite "Nippel Manager Tests":
|
|
setup:
|
|
let manager = newNippelManager()
|
|
|
|
test "Create Nippel":
|
|
let result = manager.createNippel("test-app", Workstation)
|
|
check result.isOk
|
|
check result.value.name == "test-app"
|
|
|
|
test "Activate Nippel":
|
|
discard manager.createNippel("test-app", Workstation)
|
|
let result = manager.activateNippel("test-app")
|
|
check result.isOk
|
|
check manager.isNippelActive("test-app")
|
|
|
|
test "Deactivate Nippel":
|
|
discard manager.createNippel("test-app", Workstation)
|
|
discard manager.activateNippel("test-app")
|
|
let result = manager.deactivateNippel("test-app")
|
|
check result.isOk
|
|
check not manager.isNippelActive("test-app")
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
```nim
|
|
# test_integration.nim
|
|
import unittest, nippels, profile_manager, namespace_subsystem
|
|
|
|
suite "Integration Tests":
|
|
test "Full Nippel Lifecycle":
|
|
let manager = newNippelManager()
|
|
|
|
# Create
|
|
let createResult = manager.createNippel("integration-test", Workstation)
|
|
check createResult.isOk
|
|
|
|
# Activate
|
|
let activateResult = manager.activateNippel("integration-test")
|
|
check activateResult.isOk
|
|
|
|
# Verify active
|
|
check manager.isNippelActive("integration-test")
|
|
|
|
# Deactivate
|
|
let deactivateResult = manager.deactivateNippel("integration-test")
|
|
check deactivateResult.isOk
|
|
|
|
# Remove
|
|
let removeResult = manager.removeNippel("integration-test")
|
|
check removeResult.isOk
|
|
```
|
|
|
|
### Performance Tests
|
|
|
|
```nim
|
|
# test_performance.nim
|
|
import unittest, nippels, times
|
|
|
|
suite "Performance Tests":
|
|
test "Nippel Creation Performance":
|
|
let manager = newNippelManager()
|
|
let start = cpuTime()
|
|
|
|
for i in 1..100:
|
|
discard manager.createNippel("perf-test-" & $i, Workstation)
|
|
|
|
let elapsed = cpuTime() - start
|
|
let avgTime = elapsed / 100.0
|
|
|
|
echo "Average creation time: ", avgTime * 1000, " ms"
|
|
check avgTime < 0.1 # Should be < 100ms
|
|
|
|
test "Nippel Activation Performance":
|
|
let manager = newNippelManager()
|
|
discard manager.createNippel("perf-test", Workstation)
|
|
|
|
let start = cpuTime()
|
|
discard manager.activateNippel("perf-test")
|
|
let elapsed = cpuTime() - start
|
|
|
|
echo "Activation time: ", elapsed * 1000, " ms"
|
|
check elapsed < 0.05 # Should be < 50ms
|
|
```
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
# Run all tests
|
|
nimble test
|
|
|
|
# Run specific test suite
|
|
nim c -r tests/test_nippels.nim
|
|
|
|
# Run with coverage
|
|
nim c -d:coverage -r tests/test_nippels.nim
|
|
|
|
# Run performance tests
|
|
nim c -d:release -r tests/test_performance.nim
|
|
```
|
|
|
|
## Contributing
|
|
|
|
### Contribution Guidelines
|
|
|
|
1. **Fork the repository**
|
|
2. **Create a feature branch**
|
|
```bash
|
|
git checkout -b feature/my-new-feature
|
|
```
|
|
|
|
3. **Make your changes**
|
|
- Follow code style guidelines
|
|
- Add tests for new features
|
|
- Update documentation
|
|
|
|
4. **Run tests**
|
|
```bash
|
|
nimble test
|
|
```
|
|
|
|
5. **Commit your changes**
|
|
```bash
|
|
git commit -am "Add new feature: description"
|
|
```
|
|
|
|
6. **Push to your fork**
|
|
```bash
|
|
git push origin feature/my-new-feature
|
|
```
|
|
|
|
7. **Create a Pull Request**
|
|
|
|
### Code Review Process
|
|
|
|
- All PRs require at least one review
|
|
- Tests must pass
|
|
- Documentation must be updated
|
|
- Code style must be consistent
|
|
|
|
### Areas for Contribution
|
|
|
|
- **New security profiles**
|
|
- **Additional hash algorithms**
|
|
- **UTCP protocol extensions**
|
|
- **Performance optimizations**
|
|
- **Documentation improvements**
|
|
- **Bug fixes**
|
|
- **Test coverage improvements**
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [Nippels User Guide](./NIPPELS_USER_GUIDE.md) - User documentation
|
|
- [Nippels Troubleshooting](./NIPPELS_TROUBLESHOOTING.md) - Troubleshooting guide
|
|
- [Nippels Requirements](../../.kiro/specs/nip-nippels/requirements.md) - Requirements
|
|
- [Nippels Design](../../.kiro/specs/nip-nippels/design.md) - Design document
|
|
|
|
---
|
|
|
|
**Version:** 1.0
|
|
**Last Updated:** November 19, 2025
|
|
**Status:** Developer Documentation
|
|
**Target Audience:** Developers contributing to Nippels
|
|
|