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 authenticatedGraphSession. 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 byscripts/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 byid; the thin Go client serializes one round-trip at a time (in-order). - Compact results: a top-level MessagePack
binresult is a secondRawlayer 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).