Status: Ecosystem Standard Version: 3.1.0 Authority: wateringHole (ecoPrimals Core Standards)
Version History:
- v1.0 (Jan 19, 2026): Unix socket + JSON-RPC 2.0
- v2.0 (Jan 30, 2026): Platform-agnostic transport concept
- v3.0 (Feb 3, 2026): Unified transport + dual protocol + tarpc. Consolidated from
PRIMAL_IPC_PROTOCOL.mdv2.0 andUNIVERSAL_IPC_STANDARD_V3.mdv3.0. - v3.1 (Mar 25, 2026): Wire framing, standalone startup,
--portconvention. Driven by esotericWebb first live composition with plasmidBin primals.
Primals are autonomous organisms that communicate via PROTOCOLS, not by embedding each other's code.
- Each primal implements IPC independently in its own codebase
- No shared IPC crate — duplication is healthy independence
- Standard protocol enables interoperability without coupling
- Primals adopt standard updates at their own pace
Request:
{
"jsonrpc": "2.0",
"method": "domain.operation",
"params": { "key": "value" },
"id": 1
}Success Response:
{
"jsonrpc": "2.0",
"result": { "data": "value" },
"id": 1
}Error Response:
{
"jsonrpc": "2.0",
"error": { "code": -32601, "message": "Method not found", "data": null },
"id": 1
}Standard Error Codes: -32700 (parse), -32600 (invalid request), -32601 (method not found), -32602 (invalid params), -32603 (internal).
Method naming: See SEMANTIC_METHOD_NAMING_STANDARD.md for domain.verb conventions.
The canonical wire framing for inter-primal JSON-RPC is newline-delimited:
one JSON-RPC object per line, terminated by \n (ASCII 0x0A).
{"jsonrpc":"2.0","method":"health.liveness","id":1}\n
{"jsonrpc":"2.0","result":{"status":"alive"},"id":1}\n
Primals MUST accept this framing on at least one transport (UDS or TCP).
This is the format that orchestrators, springs, and inter-primal clients use
on raw byte streams (UnixStream, TcpStream).
HTTP-wrapped JSON-RPC (e.g. POST /jsonrpc with Axum or jsonrpsee) is
acceptable for external APIs, dashboards, and human-facing tooling. However,
HTTP MUST NOT be the only JSON-RPC surface. Every primal MUST provide a
newline-delimited endpoint for composition.
| Surface | Framing | Required | Audience |
|---|---|---|---|
| Inter-primal IPC | Newline-delimited over UDS/TCP | MUST | Primals, springs, orchestrators |
| External API | HTTP POST JSON-RPC | MAY | Dashboards, CLI tools, browsers |
| High-performance | tarpc (binary) | MAY | Rust-to-Rust hot paths |
Why this matters: esotericWebb's PrimalClient sends raw newline JSON-RPC
over TcpStream and UnixStream. Primals that only accept HTTP-wrapped
JSON-RPC (e.g. behind Axum) are unreachable by the standard client. The same
applies to any orchestrator or spring using the standard raw-stream pattern.
Reference implementation: esotericWebb webb/src/ipc/client.rs demonstrates
the canonical client. The server side is a BufReader loop reading lines from
an accepted stream connection.
| Protocol | Strengths | Use Case |
|---|---|---|
| JSON-RPC 2.0 | Human-readable, debuggable, universal | External APIs, cross-language, required |
| tarpc | High-performance, type-safe, Rust-native | Internal primal-to-primal, optional |
Primals MUST support JSON-RPC 2.0. tarpc is optional for high-performance paths. Protocol negotiation occurs on connection — peers exchange supported protocols and select the best mutual option.
Tier 1 (Native — fastest for platform):
├── Unix Sockets (Linux, macOS, BSD)
├── Abstract Sockets (Android, Linux)
├── Named Pipes (Windows)
└── XPC (iOS, macOS)
Tier 2 (Universal — always works):
└── TCP Localhost (all platforms)
Tier 3 (Specialized):
├── In-Process (WASM, embedded)
└── Shared Memory (high-performance, requires setup)
Primals discover the best transport at runtime. Prefer Tier 1 (native), fall back to Tier 2 (TCP). No hardcoded socket paths.
$XDG_RUNTIME_DIR/biomeos/<primal>.sock
When running family-scoped (multiple families on one host):
$XDG_RUNTIME_DIR/biomeos/<primal>-${FAMILY_ID}.sock
For springs in niche deployment:
$XDG_RUNTIME_DIR/biomeos/<spring>.sock
Filesystem sockets are REQUIRED on Linux. Abstract namespace sockets
(@primal) are acceptable as Tier 1 on Android but MUST NOT be the only
socket on Linux — they are invisible to readdir() and break filesystem-based
discovery. Primals using abstract sockets MUST also bind a filesystem socket.
Capability-domain symlinks (v3.1): Primals SHOULD create a symlink named after their capability domain alongside the primal-named socket:
$XDG_RUNTIME_DIR/biomeos/ai.sock -> squirrel.sock
$XDG_RUNTIME_DIR/biomeos/dag.sock -> rhizocrypt.sock
This enables capability-based filesystem discovery (Tier 3 in
CAPABILITY_BASED_DISCOVERY_STANDARD.md) without requiring Songbird or
Neural API to be running.
Custom directories are non-conformant. Sockets MUST live in the biomeos
directory, not in primal-specific directories (e.g., /run/user/1000/petaltongue/
is non-conformant; use /run/user/1000/biomeos/petaltongue.sock instead).
| Platform | Transport | Status |
|---|---|---|
| Linux (x86_64) | Unix Socket | Production |
| Linux (aarch64) | Unix Socket | Production |
| Android | Abstract Socket | Production |
| macOS | Unix Socket | Production |
| Windows | Named Pipe / TCP | Planned |
| iOS | XPC | Documented |
| WASM | In-Process | Documented |
Songbird maintains the service registry. Primals communicate with Songbird via JSON-RPC — never by importing Songbird code.
{
"jsonrpc": "2.0",
"method": "ipc.register",
"params": {
"name": "beardog",
"endpoint": "/primal/beardog",
"capabilities": ["crypto", "btsp", "ed25519", "x25519"],
"version": "2.7.0"
},
"id": 1
}{
"jsonrpc": "2.0",
"method": "ipc.find_capability",
"params": { "capability": "crypto" },
"id": 2
}Returns all primals providing that capability with their endpoints.
| Method | Purpose |
|---|---|
ipc.register |
Register primal + capabilities |
ipc.resolve |
Find primal by name |
ipc.find_capability |
Find primals by capability |
ipc.list |
List all registered primals |
ipc.heartbeat |
Periodic presence signal (every 30-60s) |
When biomeOS is running, primals also register via lifecycle.register with
the Neural API. biomeOS provides higher-order routing beyond Songbird's
capability-based discovery.
Each primal implements IPC in its own codebase. This is reference code to copy and adapt — NOT a shared dependency.
your-primal/
└── src/
├── ipc/
│ ├── mod.rs # Transport selection
│ ├── server.rs # JSON-RPC server
│ └── client.rs # Client for calling other primals
└── dispatch.rs # Method routing
// Start IPC server with runtime transport discovery
let endpoint = TransportEndpoint::for_primal("myprimal")?;
let listener = bind_transport(&endpoint).await?;
// Register with Songbird
register_with_songbird("myprimal", &endpoint, &capabilities).await?;
// Accept connections
loop {
let (stream, _) = listener.accept().await?;
tokio::spawn(handle_json_rpc(stream));
}// Discover and connect (capability-based)
let crypto_endpoint = find_by_capability("crypto").await?;
let stream = connect_transport(&crypto_endpoint).await?;
// Call method
let response = json_rpc_call(&stream, "crypto.sign", json!({
"data": "hello",
"algorithm": "ed25519"
})).await?;- ZERO imports from other primals
- ZERO shared IPC crates
- Uses
tokiofor async I/O (ecosystem standard runtime) - Runtime transport discovery (not compile-time only)
- Graceful fallback (Tier 1 → Tier 2)
- ~500-1000 lines of IPC code per primal — this is healthy independence
Primals MUST start successfully when no other primals are running and no identity environment variables are set. This is standalone mode — the primal operates in isolation with degraded capability.
Primals MUST NOT hard-fail on missing FAMILY_ID, NODE_ID, or discovery
endpoints. When identity is absent, default to standalone or default.
When Songbird or Neural API are unreachable, log a warning and continue.
This enables:
- Local development without a full ecosystem
- Spring-driven composition where primals start before discovery is available
- Incremental bringup of deploy graphs
- Testing individual primals in isolation
Autonomy (critical):
- ZERO imports from other primals
- Implements IPC in own codebase
Protocol (v3.1):
- JSON-RPC 2.0 supported (required)
- Newline-delimited framing on at least one transport (UDS or TCP)
- Method naming follows
SEMANTIC_METHOD_NAMING_STANDARD.md -
health.liveness,health.readiness,health.checkrespond correctly
Transport (v3.1):
- Filesystem socket in
$XDG_RUNTIME_DIR/biomeos/<primal>.sock - Capability-domain symlink created (e.g.,
ai.sock -> squirrel.sock) - No abstract-namespace-only sockets on Linux
-
server --port <PORT>binds TCP JSON-RPC (newline-delimited) - Runtime transport discovery
- No hardcoded socket paths
Startup (v3.1):
- Starts in standalone mode without
FAMILY_ID/NODE_ID - Degrades gracefully when Songbird / Neural API are unreachable
-
server --port <PORT>accepted by UniBinserversubcommand
Discovery:
- Registers with Songbird on startup (when available)
- Declares capabilities in registration
- Periodic heartbeat (when registry is available)
Testing:
- Tested on Linux (Unix sockets)
- Tested with raw newline JSON-RPC client (not just HTTP)
See IPC_COMPLIANCE_MATRIX.md for per-primal status.
SEMANTIC_METHOD_NAMING_STANDARD.md— Method naming (domain.verb)UNIBIN_ARCHITECTURE_STANDARD.md— Binary structure and--portconventionECOBIN_ARCHITECTURE_STANDARD.md— Universal portabilityCAPABILITY_BASED_DISCOVERY_STANDARD.md— Capability-domain sockets and discovery tiersIPC_COMPLIANCE_MATRIX.md— Per-primal interop statusSPRING_INTEROP_LESSONS.md— Learnings from spring compositionbirdsong/BIRDSONG_PROTOCOL.md— Encrypted UDP discoverySPRING_AS_NICHE_DEPLOYMENT_STANDARD.md— Spring niche IPC requirements