External connectors protocol
External connectors let Kheish talk to user-authored sidecars without rebuilding the daemon. They are intended for:- chat transports such as Discord, Matrix, Signal, WhatsApp, or email
- generic webhook bridges
- domain events that still need to materialize durable Kheish sessions
- one stable runtime sidecar contract for
manifest,health, anddeliver - one additive ingress contract for daemon-owned event submission
Versioning
There are currently two version numbers in play:- runtime sidecar contract:
protocol_version = 1 - ingress contract:
protocol_version = 2, while the daemon still accepts ingress1
intent, relation, routing_key, or batch submission.
Current compatibility rules:
GET /manifestandGET /healthmust return runtimeprotocol_version = 1POST /delivercurrently receives runtimeprotocol_version = 1POST /eventsandPOST /events/batchaccept ingressprotocol_version = 1or2- the bundled Python sidecar helpers now submit ingress
protocol_version = 2by default and automatically fall back to ingress1when talking to an older daemon
Runtime-managed external connectors
Runtime CRUD uses the normal connector surface:GET /v1/runtime/connectorsGET /v1/runtime/connectors/external/{name}PUT /v1/runtime/connectors/external/{name}DELETE /v1/runtime/connectors/external/{name}
platformmode:remote_httporchild_processbase_urlshared_tokenallow_unauthenticated_ingressfixed_session_idinclude_self_outputadditional_reply_targetsadditional_binding_keyssession_policyingress_events_per_secondchild_process
child_process accepts:
commandargsenvcredential_slotsworking_dir
Sidecar runtime endpoints
One external sidecar exposes:GET /manifestGET /healthPOST /deliver
POST /__test/inject
/__test/inject is disabled by default and only turns on when all of these are true:
KHEISH_EXTERNAL_CONNECTOR_ENABLE_TEST_API=trueKHEISH_EXTERNAL_CONNECTOR_TEST_MODE=true- the sidecar
base_urlis loopback-only - the sidecar has a configured shared token
Ingress endpoints
The daemon-owned external ingress surface is:POST /v1/connectors/external/{name}/eventsPOST /v1/connectors/external/{name}/events/batch
shared_token unless:
allow_unauthenticated_ingress=true
- per-connector rate limiting through
ingress_events_per_second - request body limits
- per-event
content,input_items,attachments, and inline-asset limits - idempotency by
event_id - payload-fingerprint mismatch rejection for reused
event_id
Child-process credential bootstrap
shared_token is the normal transport credential for ingress and delivery. It is not the same thing as the child-process credential lease used for credential_slots.
When an external connector runs in child_process mode and declares credential_slots, the daemon starts the sidecar with:
KHEISH_EXTERNAL_CONNECTOR_CREDENTIAL_TOKENKHEISH_EXTERNAL_CONNECTOR_CREDENTIAL_KEYS_JSON
GET /v1/connectors/external/{name}/credentials/{env_key}
Authorization: Bearer $KHEISH_EXTERNAL_CONNECTOR_CREDENTIAL_TOKEN.
Successful responses return:
valuelease_idgrant_idexpires_at_ms
- the sidecar never receives the daemon’s long-lived root secrets directly in its startup environment
- restarting or shutting down the child revokes the active credential lease
- rotating the root secret slot does not require changing the sidecar bootstrap contract
Single-event ingress
POST /events accepts:
Fields
instance_id: opaque sidecar instance identifier; child-process ingress is validated against the sidecar manifestevent_id: durable idempotency key inside one connectorfingerprint: optional extra hash or upstream event revision used to reject conflicting replaysoccurred_at_ms: optional source timestampactor_id: optional upstream actor identifiersource_kind: free-form source family such asdiscord,email,webhook,grafana_alertintent: optional free-form semantic hint such asmessageordomain_eventrelation: optional typed relation with:kindtarget_event_id
thread.path: hierarchical conversation identityrouting_key: optional non-thread conversation key for threadless eventscontent: canonical human-readable summary of the eventinput_items: multimodal prompt itemsattachments: durable inbound filesreply_route: opaque connector-defined reply handle payloadmetadata: opaque JSON bag preserved into run input metadata
Session resolution
External ingress materializes or resolves one session in this order:- connector
fixed_session_id - session already bound to the derived external binding keys
- natural session id derived from
thread.path - natural session id derived from
routing_key
- if
thread.pathis present, it drives session identity routing_keyis only used for session identity whenthread.pathis emptyrouting_keyis still persisted into metadata when supplied
Typed semantics
intent, relation, and routing_key are accepted and preserved by the daemon.
They are currently projected into reserved run input metadata fields:
external_protocol_versionexternal_intentexternal_relationexternal_routing_key
Content, input items, and attachments
Use these fields consistently:content: the concise text an agent should always be able to readinput_items: multimodal items that should enter the prompt surface directlyattachments: durable files or binary blobs that should be preserved as attachments
- always send a meaningful
contentsummary, even for domain events - use
input_itemsfor images/audio/documents that should be directly consumable by the model - use
attachmentsfor durable file preservation
Batch ingress
POST /events/batch accepts one shared protocol_version and an ordered events array:
- request order is preserved in the response
- there is no atomicity
- each event consumes rate limit independently
- each event goes through the same validation, idempotency, and session-resolution pipeline as
/events - oversized batches are rejected at the HTTP layer
acceptedduplicaterejectedrate_limitederror
- backfills
- imports
- low-latency round-trip reduction when events are already buffered
Single-event responses
POST /events returns:
status values:
acceptedduplicaterejected
- rate limiting on
POST /eventsis still an HTTP429 - rate limiting inside
POST /events/batchis item-scoped and returnsrate_limited
Delivery
Daemon-to-sidecar delivery is unchanged by the current ingress semantics.POST /deliver still sends runtime protocol_version = 1 with:
delivery_idattemptreply_routeconversationcontentpartsartifactsmetadata
Bundled Python helper surface
The Python sidecar helper in connectors/python/common.py exposes:submit_event(payload)submit_events_batch(payloads)
- the ingress protocol version
- the sidecar
instance_id - connector auth headers when configured
Recommended sidecar patterns
- For chat transports, use
thread.pathandreply_route. - For threadless sources, prefer
routing_keyover fake thread coordinates. - For edits, replies, or reactions, fill
relation. - Keep
contenthuman-readable even when the authoritative source data lives inmetadata. - If one source can emit both chat-like and domain-like events, keep
source_kindstable and varyintent.
