Skip to main content

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:

KindModeIngestOutbound
SLACKduplex
EMAILoutbound
DOCread_only
CALENDARmanual

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:

  1. Outbound must be enabled on the source (else 403).
  2. The policy engine must not block it. A DENY is a hard 403; a policy REQUIRE_APPROVAL parks the send until an admin resolves it in the Control Tower.
  3. The provenance floor (below) — a policy ALLOW is necessary but not sufficient.
  4. Brokered, checksummed delivery — the send is enqueued as a ConnectorDelivery and driven; delivered:true is 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 pinned SKILL_VERSION (the server derives the payload),
  • the exact source + destination,
  • a rate budget (rateBudget per rateWindowSeconds) — 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 → DELIVERING under a lease (leaseExpiresAt), so two racing drains produce exactly one provider post. An expired DELIVERING lease recovers to PENDING — with the attempt already counted — so a crashed drain never wedges the row.

  • Provider-authoritative idempotent retries. A transient failure retries from nextAttemptAt with exponential backoff. The stable provider idempotencyKey makes 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-Key HTTP header; Resend returns the original message on a same-key retry (24h window).
    • Slackchat.postMessage has no native idempotency key (Slack silently ignores unknown headers), so the transport stamps the key into the message metadata.event_payload and probes conversations.history before every post. If a message already carries this key, it returns that message's ts without re-posting — Slack's own stored message is the arbiter. (This probe needs a *:history read 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).

  • 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 sendOutboundAction server action (a session shim over sendOutbound, source: "ui").
  • MCP — the send_communication tool (admin scope + workspace ADMIN role, source: "mcp"). It takes { messageId, body, to? } plus the standard optional idempotencyKey, 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 policy ALLOW parks at REQUIRE_APPROVAL (naming its provenance) unless a bound approval or a covering send route authorizes the exact payload — privilege is not authority. delivered:true is 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.