plexus-protocol: Plexus RPC protocol types and client

[ library, mit, network, unclassified ] [ Propose Tags ] [ Report a vulnerability ]

Haskell types and a client for Plexus RPC, a streaming JSON-RPC 2.0 protocol: stream items and provenance, recursive plugin and method schemas, authentication, and a WebSocket transport. Shared by Synapse and other Plexus RPC clients.


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.3.0.0, 0.3.2.0, 0.5.0.0, 0.5.2.0, 0.5.5.0, 0.6.0.0
Change log CHANGELOG.md
Dependencies aeson (>=2.0), aeson-casing (>=0.2), async (>=2.2), base (>=4.17 && <4.20 || >=4.21 && <4.22), bytestring (>=0.11), case-insensitive (>=1.2), containers (>=0.6), crypton-x509-store (>=1.6 && <1.10), crypton-x509-system (>=1.6 && <1.10), directory (>=1.3), filepath (>=1.4), network (>=3.1.2), resource-pool (>=0.4), scientific (>=0.3), stm (>=2.5), streaming (>=0.2), text (>=2.0), time (>=1.9), tls (>=2.1 && <2.5), websockets (>=0.13) [details]
Tested with ghc ==9.8.4, ghc ==9.12.4
License MIT
Copyright (c) 2025-2026 hypermemetic
Author hypermemetic
Maintainer hypermemetic@proton.me
Uploaded by hypermemetic at 2026-11-04T23:33:05Z
Category Network
Distributions
Reverse Dependencies 2 direct, 0 indirect [details]
Downloads 85 total (23 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2026-11-05 [all 1 reports]

Readme for plexus-protocol-0.6.0.0

[back to package description]

plexus-protocol

Haskell types for Plexus RPC — shared between Synapse and other Plexus RPC clients.

Layers

┌─────────────────────────────────────────┐
│  Plexus.Types                           │  Stream items, provenance
│  Plexus.Schema.Recursive                │  PluginSchema, MethodSchema
├─────────────────────────────────────────┤
│  Substrate.Transport                    │  RPC calls, schema fetching
│  Substrate.Client                       │  WebSocket connection
└─────────────────────────────────────────┘

Substrate = transport layer modules (WebSocket, JSON-RPC) Plexus = Plexus RPC protocol types (schemas, stream items)

Usage

import Plexus
import qualified Streaming.Prelude as S

main :: IO ()
main = do
  conn <- connect defaultConfig
  S.print $ substrateRpc conn "health.check" (object [])
  disconnect conn

wss (SCAFFOLD-21, 0.5.2.0)

A service behind TLS (a hosted app: wss://<host>/ws) is dialed when substrateTls is Just: the tls package over a TCP socket, SNI and the name check against substrateHost, the chain against the system's trust store (crypton-x509-system; SYSTEM_CERTIFICATE_PATH overrides it) plus each PEM file in tlsCaFiles. Verification cannot be turned off. defaultConfig is Nothing (plain ws://), as before; the upgrade's Host is hostHeader (the host alone on the scheme's default port).

let cfg = (defaultConfig "hostdemo")
      { substrateHost = "hostdemo.menger.sh", substratePort = 443
      , substratePath = "/ws", substrateTls = Just defaultTlsOptions }
Right items <- rpcCallWith cfg "_info" Null

Routing calls (SYNAPSE-28, 0.5.3.0)

Plexus.Transport.setCallRouter sends the calls whose config the router answers Just for some other way than this process's pools — synapse uses it to put every call of a run through a connection held between runs, or through one connection the run shares. A route reports failures as connect and substrateRpcWith do (LostConnection with whether the request had gone out; a failed dial as an error call), so everything above it reads them as before. A program that never sets it is unchanged.

Plexus.Client.substrateRpcSub is substrateRpcWith telling the caller the subscription's id, and unsubscribe conn "<backend>.call_unsub" sid ends a call it no longer reads (a number id is sent as a number) — for a connection shared by many calls, where one abandoned must not keep streaming.

Schema Fetching

import Substrate.Transport

-- Fetch plugin schema
Right schema <- fetchSchemaAt defaultConfig ["echo"]
print $ psNamespace schema  -- "echo"
print $ psMethods schema    -- [MethodSchema...]

-- Fetch method schema directly
Right method <- fetchMethodSchemaAt defaultConfig ["echo"] "once"
print $ methodName method   -- "once"
print $ methodParams method -- Just (JSON Schema)

Types

Transport Errors (v0.3.0+)

Strongly-typed errors for transport-layer failures:

data TransportError
  = ConnectionRefused { transportHost :: Text, transportPort :: Int }
  | ConnectionTimeout { transportHost :: Text, transportPort :: Int }
  | ProtocolError     { transportMessage :: Text }
  | NetworkError      { transportMessage :: Text }

All transport functions return Either TransportError a instead of Either Text a:

-- Schema fetching
fetchSchemaAt :: SubstrateConfig -> [Text] -> IO (Either TransportError PluginSchema)

-- Method invocation
invokeMethod :: SubstrateConfig -> [Text] -> Text -> Value
             -> IO (Either TransportError [PlexusStreamItem])

-- Streaming invocation
invokeMethodStreaming :: SubstrateConfig -> [Text] -> Text -> Value
                      -> (PlexusStreamItem -> IO ())
                      -> IO (Either TransportError ())

Benefits:

  • No string parsing to categorize errors
  • Structured error information (host, port, message)
  • Type-safe error handling
  • Proper exception categorization using IOException matching

Example:

result <- fetchSchemaAt cfg ["echo"]
case result of
  Left (ConnectionRefused host port) ->
    putStrLn $ "Can't connect to " <> host <> ":" <> show port
  Left (ConnectionTimeout host port) ->
    putStrLn $ "Timeout connecting to " <> host <> ":" <> show port
  Left (ProtocolError msg) ->
    putStrLn $ "Protocol error: " <> msg
  Left (NetworkError msg) ->
    putStrLn $ "Network error: " <> msg
  Right schema ->
    print schema

Stream Items (plexus 0.7)

data PlexusStreamItem
  = StreamProgress Text Provenance Text Value        -- hash, provenance, content_type, content (the method's progress P)
  | StreamData     Text Provenance Text Value        -- hash, provenance, content_type, content (T, or {"ok": T} | {"err": E})
  | StreamGuidance …
  | StreamError    Text Provenance RemoteError Bool  -- hash, provenance, error, recoverable
  | StreamRequest  …
  | StreamDone     Text Provenance

An error frame means the call itself failed, and says why with a RemoteError (Plexus.RemoteError, mirroring plexus_client::RemoteError; renderRemoteError is its one-line rendering). A JSON-RPC rejection carries the same type in errData. An activation saying no is a data item whose content is {"err": …}.

data RemoteError
  = ActivationNotFound Text            -- activation
  | MethodNotFound Text Text           -- activation, method
  | InvalidParams (Maybe Text) ParamProblem
  | Unauthenticated
  | Forbidden DenyReason
  | HandleNotSupported Text            -- activation
  | Execution Text                     -- message

data ParamProblem = Missing | Malformed Text
data DenyReason   = MissingScope Text | TenantBoundary | NotAccepted

Schema Types

data PluginSchema = PluginSchema
  { psNamespace       :: Text
  , psVersion         :: Text
  , psDescription     :: Text
  , psLongDescription :: Maybe Text
  , psSelfHash        :: Maybe PluginHash
  , psHash            :: PluginHash
  , psMethods         :: [MethodSchema]
  , psChildren        :: Maybe [ChildSummary]
  , psDeprecation     :: Maybe DeprecationInfo
  }

-- plexus 0.7: a method's contract is T, E?, P? and its cardinality
data MethodSchema = MethodSchema
  { methodName        :: Text
  , methodDescription :: Text
  , methodHash        :: PluginHash
  , methodParams      :: Maybe Value  -- JSON Schema
  , methodReturns     :: Value        -- JSON Schema of the item type T (required)
  , methodError       :: Maybe Value  -- JSON Schema of the error type E (fallible methods)
  , methodProgress    :: Maybe Value  -- JSON Schema of the progress type P
  , methodCardinality :: Cardinality  -- CardinalityOne | CardinalityMany (required)
  , …                                 -- bidirectional, http_method, request/response types, deprecation, role
  }

data SchemaResult
  = SchemaPlugin PluginSchema
  | SchemaMethod MethodSchema

Structure

src/
├── Plexus.hs                   # Re-exports
├── Plexus/
│   ├── Types.hs                # PlexusStreamItem, Provenance
│   ├── Schema.hs               # Schema re-exports
│   └── Schema/
│       ├── Recursive.hs        # PluginSchema, MethodSchema, SchemaResult
│       └── Cache.hs            # Schema caching
└── Substrate/
    ├── Client.hs               # WebSocket connection
    └── Transport.hs            # RPC calls, schema fetching

Build

cabal build
cabal test                    # test-suite naming-gate
scripts/naming-gate.sh        # the naming gate: each line that says the retired word (-c: the count)

A Plexus backend is a service, and an activation with children a parent (isParentActivation; NAMING-9, ~/AGENTS.md "Naming"). The suite runs the gate, which passes only lines marked naming-alias: the deprecated old name of isParentActivation, kept until NAMING-18.

The tests read the plexus 0.7 conformance fixture (RETURNS-7) by sibling path, ../plexus-macros/fixtures/conformance/expected/: check out plexus-macros next to this repository first.

License

MIT