Skip to content

Client drivers (CONCEPT:EG-KG.ingest.broker-streams-namespaces)

The Python package provides the complete network client and the release wheel's in-process engine binding. JavaScript and Go provide focused bindings for the native message broker, append-log streams, RBAC administration, online backup/restore, and NL→query surfaces.

Network clients use out-of-process framed MessagePack over UDS/TCP. For that deployment shape, the wire is the API: Python, JavaScript, and Go mirror the serde-tagged Method enum in crates/eg-types/src/protocol.rs by sending the variant name and its exact parameter fields. The separate epistemic_graph.engine binding embeds the Rust graph core directly for single-process use and does not traverse the wire.

Full vs thin, per language

Restricting Python credentials to a local recipient

Applications that hold a process proof for one local engine must bind the client before its first connection, even if that connection starts without the proof:

client = await EpistemicGraphClient.connect(
    socket_path=recipient,
    required_socket_path=recipient,
    auth_secret=auth_secret,
    verified_context=transport_context,
)
with client.use_verified_context(
    current_claims,
    required_socket_path=recipient,
    write_admission=admit_current_claims,
):
    await client.health()

recipient must be an explicit canonical absolute filesystem Unix socket path. Relative paths, abstract sockets, URI spellings, symlinks, and non-normalized paths are refused. Resolve the trusted endpoint before creating the client. Omitting socket_path uses required_socket_path directly. A conflicting socket, any TCP selection, or TLS configuration is refused. Missing sockets fail without consulting environment defaults or the conventional /tmp fallback.

The restriction applies to every request and reconnect for that client, including claims provided later through use_verified_context. A task-local restriction must equal the connection's restriction; it cannot upgrade an existing unbound client. Create a separately bound client instead. Claims remain task-local and the synchronous client forwards both APIs, including calls through namespaces. The client checks the connected transport and connection generation after reconnection and again after acquiring write admission, before any frame bytes. A changed admission fails the call; retry only after revalidating application authority. Recipient metadata is never added to the request envelope.

A bound context containing oidc_token also requires write_admission. Both connect() and use_verified_context() accept this synchronous factory: Callable[[RequestContextClaims], ContextManager[None]]. It receives a defensive copy of the exact claims serialized for that request. Its context manager must acquire the same lock used for atomic credential renewal, check that the current checked proof and authority still match those claims and the intended recipient, and reject expired or superseded proof. It must hold that lock through yield. The client enters it after any reconnect and write-lock wait, checks the actual recipient again, and writes both frame parts without awaiting. It releases admission before awaiting the transport drain or response.

This is fail-closed admission: if token A was serialized and the lease has since expired or renewed to B, the queued A request is refused before any bytes. The caller may start a new request with a fresh projection; the client does not automatically replay or re-sign it. Claims and their admission factory remain paired through concurrent tasks, synchronous calls, and native codec preparation. A task-local context must provide its own factory; it never inherits another session's factory from the connection. Proof-free transport setup needs none.

Admission factories must be short, synchronous, reusable, and perform no network I/O or nested RPC. Their managers must yield None and exit synchronously without raising. Never hold the renewal lock while calling a synchronous client: the client's event-loop thread must acquire it. Entry refusals become a sanitized PermissionError, clean up the pending call, and leave a healthy transport usable. Cleanup failure is transport-fatal because bytes may already have been written. No callback or proof material is included in diagnostics or wire fields.

Bound clients refuse fresh_bolt_auth_token(), because an exported credential can be used on a separate connection outside this restriction. Existing clients without required_socket_path retain their normal remote transport behavior. This is local endpoint confinement, not cryptographic peer identity: deployments must protect the socket and its parent directories against replacement. Proof validation, issuer/audience policy, and credential renewal remain required.

Language Location Scope Tested
Python epistemic_graph/client.py Full — graph/vector/RDF/SQL/txn/broker plus governed modalities and native knowledge streaming. tests/test_pb_clients.py, tests/test_modality_stream_clients.py, and the gen_contract --check engine-contract gate.
Python embedded epistemic_graph.engine in the release wheel In-process graph creation and node operations for explicit local/ephemeral use. Wheel-completeness and transport-parity gates.
JS / Node clients/js Thin — ONLY the B1.7 methods, generated from the Method list. Not a full SDK. Current eg2. binding; run the package tests before release.
Go clients/go Thin — ONLY the B1.7 methods, generated from the Method list. Not a full SDK. Current eg2. binding; run go test ./... before release.

The thin clients are honest reference bindings: they carry the transport (framing + HMAC auth + result decode) and the B1.7 method surface, and each README states plainly that the full graph/vector/RDF/SQL API is Python-only. No faked SDK.

Governed Python namespaces

The complete Python client binds the two served protocols directly:

Namespace Current operation Result
client.knowledge pull(query, batch_size=..., cursor=...) One arrow_ipc payload and an authority-/placement-/snapshot-bound cursor
client.modalities authority() Opaque tenant, access-policy, and purpose references derived from the checked request
client.modalities ingest(...) Atomic native decode/create/update outcome
client.modalities query(...) Bounded page of active or authorized cold records
client.modalities search_documents(...), query_image_region(...), query_similar_images(...), query_audio_window(...), query_video_window(...) Bounded native-posting query with exact policy and predicate filtering
client.modalities delete(...), move_to_cold(...), restore(...) Versioned governed lifecycle outcome
client.modalities events(...) Bounded, ordered, policy-filtered replay events
client.modalities capabilities(modality) Exact 12 PASS / 0 N/A component result; release readiness comes from G-14/G-37 evidence
batch = await client.knowledge.pull(
    {"family": "graph", "label": "Capability", "limit": 100},
    batch_size=32,
)
arrow_ipc = batch["payload"]

authority = await client.modalities.authority()
page = await client.modalities.query(
    "document",
    segment_kind="paragraph",
    limit=100,
)

KnowledgeStream has one projection (arrow_ipc) and one pull method. The binding does not expose an alternate projection or direct-family aliases. It validates the complete query, cursor, and response shape, including matching family and batch size. The modality binding likewise emits only the current tagged operation shapes and rejects unknown fields, retired segment kinds, malformed opaque references, drifted artifact-bundle tiers, and non-certified capability reports. The synchronous client exposes the same namespaces and method names.

For create-once coordination, Python exposes await client.nodes.create_if_absent(node_id, properties). It sends the sole CreateNodeIfAbsent operation and returns True only to the inserting writer; it does not compose has and add, so there is no client-side check-then-write race.

For ingest, bundle_msgpack is a certified ArtifactBundle encoded as MessagePack; source_bytes is ephemeral request content. URLs, credentials, filesystem paths, tenant names, and deployment profiles are not fields in either operation. Connection and trust configuration remains outside these protocol payloads.

Wire contract (all three)

  • Framing: a 4-byte big-endian length prefix + a MessagePack request { id, graph, auth_token, method, params }.
  • Auth: every driver requires complete checked-context claims and signs the sole eg2. identity/policy envelope. Arbora binds those claims from the immutable authenticated GraphSession. There is no anonymous or reduced-claim client mode. See Service mode.
  • Canonical body (JS/Go): the MAC covers the body the engine re-derives from the request it DECODED (declaration order, serde defaults, sorted maps, float/byte widths), not the bytes sent. The thin clients compute it with the engine's own decoder and encoder: the embedded eg_method_codec.wasm (crates/eg-method-codec, built by scripts/build_method_codec_wasm.py). Each client's vector test replays every contract method-body vector (body digest + engine envelope MAC) through its signer. A request the engine can not decode is refused before it is sent.
  • Correlation: each response carries the request id. The Python client demuxes out-of-order responses on one pipelined connection (EG-043); the thin JS client does the same by id; the thin Go client serializes one round-trip at a time (in-order).
  • Compact results: a top-level MessagePack bin result is a second Raw layer and is decoded once more.

Methods covered (B1.7)

Domain Engine Methods Concept Python JS Go
Broker admin DeclareExchange DeleteExchange DeclareQueue BindQueue UnbindQueue EG-275/276/277/278 client.broker.* ✓ ✓
Broker publish Publish PublishEx PublishConfirmed PublishIdempotent EG-275/279/284/314 client.broker.publish* ✓ ✓
Broker consume BrokerConsume BrokerAck BrokerReject BrokerAckTag BrokerNackTag BrokerRenewTag SweepExpired EG-KG.compute.groups-qos-prefetch-honoring/276/284 client.broker.consume/ack_tag/nack_tag/renew_tag/… ✓ ✓
Streams StreamDeclare StreamPublish StreamRead StreamTrim StreamCommitOffset StreamCommittedOffset EG-283 client.broker.stream_* ✓ ✓
RBAC admin RbacAdmin (AddRole/RemoveRole/AddGrant/RemoveGrant/List) EG-KG.compute.feature client.rbac.* ✓ ✓
Ops Backup Restore EG-090 client.admin.* ✓ ✓
NL→query NlQuery NlQueryExplain EG-080 client.query.nl_query / nl_query_explain ✓ ✓

Every served build requires security. Other ops remain feature-gated on the server: broker (broker + streams), redb (backup/restore), and nl-query plus a configured planner (NL). A build/deploy without one returns a clear "not available in this build" / "no planner configured" error — never a panic. NL→query also needs a configured NlPlanner (an OpenAI-compatible endpoint, e.g. agent-utilities' LLM); the client just carries the text.

Client → Method → engine

flowchart LR
  subgraph clients["Client drivers (CONCEPT:EG-KG.ingest.broker-streams-namespaces)"]
    PY["Python (full)\nepistemic_graph/client.py\n.knowledge / .modalities / .broker / .rbac / .admin"]
    JS["JS thin\nclients/js"]
    GO["Go thin\nclients/go"]
  end

  PY -- "framed msgpack\n(4B len + {id,graph,auth_token,method,params})\nHMAC-SHA256 auth" --> T
  JS -- "framed msgpack" --> T
  GO -- "framed msgpack" --> T

  T["Transport\nsrc/server/transport.rs\n(UDS / TCP, pipelined, id-demux)"] --> D["dispatch\nsrc/server/dispatch.rs"]

  subgraph methods["Method enum · crates/eg-types/src/protocol.rs"]
    M_BROKER["Publish* / DeclareExchange /\nDeclareQueue / BindQueue /\nBrokerConsume / BrokerAck / BrokerReject /\nBrokerAckTag / BrokerNackTag / BrokerRenewTag / SweepExpired"]
    M_STREAM["StreamDeclare / StreamPublish /\nStreamRead / StreamTrim /\nStreamCommitOffset / StreamCommittedOffset"]
    M_RBAC["RbacAdmin{op}"]
    M_BAK["Backup / Restore"]
    M_NL["NlQuery{text,graph}"]
    M_MODAL["ServedModality{op}"]
    M_KNOW["KnowledgeStream{request}"]
  end

  D --> M_BROKER
  D --> M_STREAM
  D --> M_RBAC
  D --> M_BAK
  D --> M_NL
  D --> M_MODAL
  D --> M_KNOW

  M_BROKER --> H1["handlers/graph_ops.rs\ncrate::broker (eg-core)\nEG-275..284/314"]
  M_STREAM --> H1
  M_RBAC --> H2["dispatch.rs → isolation.rbac\neg-core acl · EG-KG.compute.feature"]
  M_BAK  --> H3["handlers/admin.rs\nredb backup/restore · EG-090"]
  M_NL   --> H4["handlers/query.rs → NlPlanner\n→ UQL → run_unified · EG-078/080"]
  M_MODAL --> H5["handlers/modality.rs\nverified governed serving"]
  M_KNOW --> H6["handlers/knowledge_stream/{mod,families,stream}.rs\nsole Arrow IPC result stream"]

Parity gate

RF-RULING-003 made the contract itself the gate. crates/eg-capabilities declares one MethodDescriptor per Method variant, and cargo run -p eg-capabilities --features contract --bin gen_contract -- --check regenerates contract/methods.json, contract/schemas/*.json, contract/receipt.json, docs/capabilities.generated.md and epistemic_graph/generated/*.py in memory, then byte-diffs the committed tree. "Does the Python client send this method" is now the descriptor's consumer_profiles field, and a method with no consumer is stability: internal on its own row — so the old tests/protocol_unbound_baseline.txt ratchet is deleted rather than re-baselined. crates/eg-capabilities/tests/contract_registry.rs proves the registry and the Method enum are a bijection with no baseline, and tests/test_generated_client_contract.py (pure stdlib) proves the generated Python surface is exactly the published profile. The JS/Go thin clients are still hand-maintained; their ConsumerProfile values exist in the descriptor but no generator emits them yet.


See also: Capabilities matrix · Connecting (per-wire guide) · SQL & pgwire · Messaging & Broker · KV-cache (vLLM/LMCache).