plana 0.2.0

PLANA protocol foundation: the canonical JSON-RPC 2.0 wire layer (unix-socket / HTTP / WS bindings, typed method routing) merged with the generic protocol core (handshake, identity, RBAC, regions, health descriptors) — the base every plana-rpc-* and domain-profile crate builds on.
Documentation

plana

PLANA - Protocol for Live Agent Network Automation: a typed application-layer protocol for real-time state synchronization and control between a client shell and a backend service runtime, built on JSON-RPC 2.0 (the way HTTP is built on TCP). Not a general-purpose RPC framework.

This repo is split into four publishable crates:

Crate Role in the stack
plana-protocol-core Generic protocol core - handshake and identity negotiation, base protocol messages, health/network descriptors, RBAC, and region policy. Independent of any specific platform domain.
plana-celestia-types Celestia domain profile - the celestia-island platform's agent, task, panel, industrial and tool domain messages, built on the generic plana-protocol-core message set.
plana-jsonrpc Framing & transport base - JSON-RPC 2.0 correlation, typed method routing, and Unix-socket / HTTP / WebSocket bindings.
plana The protocol layer - re-exports the core, the domain profile and the framing, and adds server-side session management (SSE, events) behind the rpc-server feature.

Architecture: generic core + domain profiles

PLANA is layered: the generic protocol core sits at the bottom, a domain profile plugs its message vocabulary in on top of it, and the umbrella crate re-exports both for consumers.

plana-protocol-core (generic message set)  ←  plana-celestia-types (domain profile)  ←  plana (umbrella)
      └────────────────────────────────────────── plana-jsonrpc (framing; namespace! macro machinery)
  • The generic core - plana-protocol-core owns the platform-independent message set: handshake/version/identity negotiation, base protocol messages, health and network descriptors, RBAC, and region policy. It knows nothing about agents, tasks, panels or any specific platform. The generic JSON-RPC 2.0 envelope lives with the framing crate (plana-jsonrpc) as the single canonical definition - a former copy here drifted and was removed.
  • Registering a domain - two mechanisms, one for the protocol's own families and one for third-party profiles:
    • Built-in method families declare their namespaces with the namespace! macro (re-exported as plana::jsonrpc::namespace), which generates the typed Method variants whose wire names are derived from the enum path (Sync.Ping, Base.Heartbeat, …). The Method catalog itself is a closed set — extending it is a change to the protocol crate.
    • Third-party profiles do not fork or patch the protocol: they register their own method names directly into plana::jsonrpc::RpcMethodMap (RpcMethodMap::empty().method("my.domain.op", handler), string-keyed dynamic dispatch over HTTP/WS via rpc_axum_router), so any "client shell <-> backend runtime" scenario can speak the same JSON-RPC 2.0 framing without the protocol knowing its methods.
  • The celestia profile - plana-celestia-types is the celestia-island platform's domain profile, built on plana-protocol-core. It ships by default (feature celestia) because the celestia platform is PLANA's primary consumer; other consumers can disable it (default-features = false) and register their own profiles.
  • Shared module names - http and enums exist in both the core and the profile; the umbrella merges them under one path (plana::http::*), with generic and domain types side by side. The full domain surface is also reachable at plana::celestia.
  • Why it matters - the protocol is usable beyond its origin: any "client shell <-> backend runtime" state-synchronization scenario can implement its own profile without forking the protocol.

Usage

Add to your Cargo.toml — either the umbrella crate:

[dependencies]
plana = { version = "0.1", features = ["rpc-server"] }

or just the parts you need:

[dependencies]
plana-protocol-core = { version = "0.2" }
plana-celestia-types = { version = "0.1" }
plana-jsonrpc = { version = "0.1" }

Registering and calling a JSON-RPC method

Built-in method families are declared with the namespace! macro (defined in plana-jsonrpc, see pending.rs) and dispatched through the typed Method enum. Third-party method names register directly into RpcMethodMap — no enum extension needed:

use plana::jsonrpc::{JsonRpcRequest, RpcMethodMap, UnixMethod, serialize_to_jsonrpc};
use serde_json::{Value, json};

fn main() -> Result<(), serde_json::Error> {
    // A typed method name from the declaration site of your domain profile:
    let req = JsonRpcRequest::new(UnixMethod::ToolListTools, None);

    // Serialize to the wire (JSON-RPC 2.0 envelope):
    let json = serialize_to_jsonrpc(&req, false)?;

    // Third-party methods: string-keyed dynamic dispatch, no forking.
    let methods = RpcMethodMap::empty()
        .method("my.domain.op", |params: Value| async move {
            Ok(json!({ "echo": params["text"].clone() }))
        });
    assert!(json.starts_with("{\"jsonrpc\":\"2.0\","));

    // Health/network descriptors from the generic core:
    let health = plana::http::HealthResponse::ok(
        "1.0.0",
        plana::http::BackendKind::Dev,
        42,
        plana::http::NetworkInfo::unknown(),
    );
    serde_json::to_string(&health).map(|_| ())
}

Using the shared health/network wire types

use plana::http::{BackendKind, HealthResponse, NetworkInfo, ServiceStatus};

fn main() -> Result<(), serde_json::Error> {
    let health = HealthResponse {
        status: ServiceStatus::Ok,
        version: "1.0.0".to_string(),
        kind: BackendKind::Dev,
        uptime: 42,
        network: NetworkInfo {
            transport: "local".to_string(),
            region: "XX".to_string(),
            asn: None,
        },
        build_hash: None,
        engine_version: None,
    };
    serde_json::to_string(&health).map(|_| ())
}

Unix-socket transport

use std::path::Path;
use plana::jsonrpc::{JsonRpcRequest, TimeoutPolicy, unix_transport::JsonRpcTransport};

let mut transport = JsonRpcTransport::connect(Path::new("/run/my-app/rpc.sock")).await?;
let response = transport.send(&JsonRpcRequest::new_raw("ping", None), TimeoutPolicy::Default).await?;

Feature flags

Feature Default What it enables
celestia yes plana-celestia-types re-exported at the crate root and at plana::celestia; plana-protocol-core is always re-exported at the root.
rpc-server no Server-side SSE event streaming and request network/geo detection (rpc_server::detect_network). Transport sessions for SSE live in plana::jsonrpc::session.
jsonrpc no No-op compatibility feature — plana-jsonrpc is an always-on dependency now, so there is nothing to gate. Kept so consumers that declare it keep resolving.
tracing-helpers no Forwards plana-protocol-core/tracing-helpers: plana::tracing_helpers module with a ShortTimer formatting type.

Consumer migration (post re-pin)

The feature surface of plana changed when the generic core was extracted:

  • jsonrpc — no-op now (plana-jsonrpc is always on). A no-op compat feature is kept so existing declarations keep resolving; do not rely on it to disable anything.
  • types — renamed to celestia. The domain types now live in plana-celestia-types; enable celestia for the domain vocabulary.
  • tracing-helpers — forwarded to plana-protocol-core/tracing-helpers.

After re-pinning, declare the following features:

Consumer Features to declare
arona rpc-server
shittim-chest jsonrpc, tracing-helpers, celestia
evernight jsonrpc
entelecheia rpc-server, celestia (aspirational — see note below)
scriptum jsonrpc (aspirational — see note below)

entelecheia and scriptum rows are re-pin guidance, not current fact. entelecheia's manifest still pins the dead plana-types crate name (plana = { package = "plana-types", git = "…/plana", tag = "v0.1.9" }), and scriptum's Cargo.toml still pins the dead plana-types crate name as a git dependency on live master; neither has been migrated to the split plana/plana-celestia-types crates yet. The rows above are the feature sets they should declare once re-pinned.

TypeScript bindings

Generated TS bindings live in two places, both regenerated by just gen bindings:

  • packages/protocol-core/bindings/ — the generic core types (health, RBAC, handshake, region policy, base messages). The JSON-RPC envelope (owned by plana-jsonrpc) is Rust-only and has no TypeScript export. In-repo for now; an npm home for the generic bindings package is a follow-up decision.
  • packages/celestia-types/bindings/ — the celestia domain profile types (agent, task, industrial, tool, malkuth supervision, …), shipped as @celestia-island/plana-celestia-types.

The generic types (e.g. HealthResponse, RbacUser, ConnectionStatus, BaseHeartbeatParams, RegionPolicy) are exported only from packages/protocol-core/bindings/; the celestia package ships domain types only.

HealthDetailed was removed in the core split (dead in-repo, unused by in-repo consumers). If you consumed it from the old @celestia-island/plana-types npm bindings, migrate to the structured health fields (HealthResponse / ServiceStatus / ConnectionStatus).

Stability

Pre-1.0: the API is subject to change. The wire formats (serde layouts, method naming conventions) are treated carefully — changes that affect on-the-wire compatibility are always considered breaking.

Not a general-purpose RPC framework

plana is the scaffold for one specific typed bidirectional sync protocol, not a general-purpose RPC framework. The wire types and JSON-RPC machinery are shaped by that protocol's needs; for a generic RPC stack, look at established frameworks instead.