Communication connectors
Communication connectors are how Jentrix ingests stakeholder conversations (Slack threads, meetings, docs, email) into the workspace and — governed to the hilt — sends replies back out. They are the ecosystem half of the Knowledge & Ecosystem Plane: where governed memory and the skill registry decide what an agent may know and do, a connector decides what may come in (with provenance) and what may go out (with policy and attribution). The governing rule is simple and absolute:
Nothing in without provenance and review; nothing out without policy and a human-or-route authority.
Connectors are provider-optional, exactly like Pusher/R2/Resend: a deployment either wires a provider's env or it does not. An unconfigured connector is a silent no-op — ingest yields nothing, an outbound send is recorded but never delivered — so a workspace with no connector credentials behaves byte-identically to one that never had the adapter.
Sources
A CommunicationSource is an M5 SourceRecord
(1:1): its trust, freshness, privacy, owner, and project scope live on the
SourceRecord and are enforced live. The connector row adds only the
connector-specific settings and a config that may reference a credential
only by credentialRef (an M4 credential broker record
id) — never an inline secret. A source declares a kind (SLACK | EMAIL | DOC | CALENDAR) and an ingestMode; outboundEnabled must be explicitly set before a
send is even considered.
Adapters
Each kind maps to a live adapter — a pure projection of provider data with
one impure seam (deliver) that always takes an injected transport, so the
whole path is testable without a live provider:
| Kind | Mode | Ingest | Outbound |
|---|---|---|---|
SLACK | duplex | ✅ | ✅ |
EMAIL | outbound | — | ✅ |
DOC | read_only | ✅ | — |
CALENDAR | manual | — | — |
describeAdapter(kind) reports the mode, capabilities, and whether the provider
is wired up in this deployment (available) — the signal an agent reads to
tell a live Slack duplex source from a manual one.
Ingest is immutable (provenance you can pin)
A provider edit never overwrites the bytes a prior reference cited. Every
ingest — the first and each subsequent edit — appends an immutable
IngestedMessageRevision (revision number + provider revision id + sha256
content hash) and repoints the message head; the display/search body follows
the head, but the revision log is append-only. A memory candidate, launch
manifest, or audit that pinned a content hash still resolves to the exact
bytes it consumed (resolveRevisionByHash), even after the head moves. An
identical re-ingest converges (no duplicate revision) — two database unique
keys ((message, revisionNo) and (message, contentHash)) backstop it.
Governance is re-resolved live at every enforcement point: the
privacyBoundary stamped at ingest is a search hint only. Tightening a source to
CLIENT_PRIVATE (or rescoping it to a project) immediately governs
already-ingested messages — a client-private conversation can never be linked
into a different project's context.
Outbound delivery is governed and live
sendOutbound turns an approved reply into an actual provider send, without
loosening any governance. It fails closed at every layer:
- Outbound must be enabled on the source (else
403). - The policy engine must not block it. A
DENYis a hard403; a policyREQUIRE_APPROVALparks the send until an admin resolves it in the Control Tower. - The provenance floor (below) — a policy
ALLOWis necessary but not sufficient. - Brokered, checksummed delivery — the send is enqueued as a
ConnectorDeliveryand driven;delivered:trueis reported only from the provider acknowledgment.
The provenance floor
A policy ALLOW delivers a human-initiated send. The identical
agent-initiated send under the same ALLOW does not deliver — it
parks at REQUIRE_APPROVAL, naming its provenance — unless one of two
server-verifiable authorities covers it:
- a bound single-use approval for the exact payload, or
- a standing send route whose server-verified template the payload actually matches.
A free-form agent payload aimed at a routed source/recipient still parks: a route pins a content hash, not a destination, so an arbitrary body can never ride a route. This is the whole point — an agent cannot escalate a know/plan capability into an unattended outbound message on a policy ALLOW alone.
Send routes
A SendRoute is a narrow, admin-created standing authority. It pins only server-verifiable, enforceable attributes and carries no free-form intent/purpose field:
- the sending AgentProfile (and optionally a pinned release digest),
- the trigger it covers (a canonical event key),
- the payload generator — either a
TEMPLATE(a server-verified payload content hash) or a pinnedSKILL_VERSION(the server derives the payload), - the exact source + destination,
- a rate budget (
rateBudgetperrateWindowSeconds) — an exhausted route yields authority back to exact-payload approval, - an expiry, and the pinned policy revisions captured at creation — drift against the live revision suspends the route (it never silently widens).
Coverage is derived server-side from these attributes. A route is authority for exactly the payloads it can prove it produced — nothing more.
Exactly-once, fail-closed delivery
ConnectorDelivery follows the same discipline as
outbound webhooks, not a fire-and-forget relay:
-
Exact-payload binding. The payload is checksummed (
payloadHash) at enqueue and re-verified at the send boundary — a payload edited after authorization is refused on a checksum mismatch, before any provider call. The approval that unblocks a parked send binds the exact action hash, so an edit likewise fails to match and re-parks. -
Leased-CAS execution. A drain flips
PENDING → DELIVERINGunder a lease (leaseExpiresAt), so two racing drains produce exactly one provider post. An expiredDELIVERINGlease recovers toPENDING— with the attempt already counted — so a crashed drain never wedges the row. -
Provider-authoritative idempotent retries. A transient failure retries from
nextAttemptAtwith exponential backoff. The stable provideridempotencyKeymakes an ambiguous failure (the provider received the first post but the ack was lost) safe — and the dedup is enforced by the provider, not a local guess:- Email (Resend) — the key is sent as the
Idempotency-KeyHTTP header; Resend returns the original message on a same-key retry (24h window). - Slack —
chat.postMessagehas no native idempotency key (Slack silently ignores unknown headers), so the transport stamps the key into the messagemetadata.event_payloadand probesconversations.historybefore every post. If a message already carries this key, it returns that message'stswithout re-posting — Slack's own stored message is the arbiter. (This probe needs a*:historyread scope on the bot token, which the connect flow requests; absent it, Slack outbound degrades to at-least-once rather than blocking sends.)
So a lost-ack never double-posts. A double-enqueue of the same authorized send converges on the existing row (DB-unique per source).
- Email (Resend) — the key is sent as the
-
Brokered credentials, fail closed. The connector credential is resolved from the M4 broker per attempt, never stored on the delivery. Revoking it mid-delivery yields a structured failure + retry — never partial silent state. Sustained failure auto-disables outbound for the source (
outboundEnabled → false) with an audit row, and blocks any still-pending deliveries.
Terminal states are DELIVERED | FAILED | DISABLED. The sweep cron drains due
deliveries and recovers expired leases every tick, so retries fire without a
human in the loop.
Agent + UI surface
Outbound is reachable from both surfaces over the same governed core:
- UI — the
sendOutboundActionserver action (a session shim oversendOutbound,source: "ui"). - MCP — the
send_communicationtool (admin scope + workspace ADMIN role,source: "mcp"). It takes{ messageId, body, to? }plus the standard optionalidempotencyKey, and returns the delivery outcome (delivered/pending/policy/provenance/approvalEvaluationId). Because the call is agent-initiated, the provenance floor still applies: even with the admin scope, an agent send under a policyALLOWparks atREQUIRE_APPROVAL(naming its provenance) unless a bound approval or a covering send route authorizes the exact payload — privilege is not authority.delivered:trueis still reported only from the provider ack, and a workspace with no live connector behaves byte-identically to v1.
Secret-transit discipline
An adapter never holds a secret. A connector's persisted config — the JSON
on the source, the JSON returned to agents, and the JSON handed to the adapter at
runtime — may reference a credential only via credentialRef. The write
boundary rejects any secret-shaped value or well-known secret key
(assertNoSecretsInConfig), and buildConnectorConfig is the sanctioned way to
turn an OAuth/API connect result into a persistable config that stores the
reference only. Raw material is minted at the delivery boundary and never
persisted.
Attribution
An agent-authored ingest link or outbound send carries its source
(ui | mcp | automation | system) through the activity ledger, so the feed can
always separate a human reply from an agent's. Combined with the provenance
floor, that means every outbound byte is traceable to who (or what)
authorized it and which template or approval covered it.