External Conversation Bridges
Admit authenticated Slack threads into Ductor's durable agent runtime with explicit actor bindings, ordered processing, same-thread replies, and single-use browser approvals.
An external conversation bridge lets a messaging thread act as a client of Ductor's durable agent runtime. Slack is the first supported provider. The bridge verifies the request, resolves tenant and actor identity from server-owned connector state, durably admits the message, and returns the result to the same thread through a governed connector action.
Enable the bridge
The Slack webhook is part of enabled durable chat. Configure chat, inference, Postgres, connector encryption/storage, an operational tool backend, and the Slack action executor before startup:
aichat:
enabled: true
durable_runtime: true
model: gemini-2.5-flash-lite
max_iterations: 8
max_tool_calls: 16
# Optional: browser decisions for approval-gated Slack turns.
approval_links_enabled: true
approval_links_base_url: https://ductor.example.com
approval_link_ttl: 15mapproval_links_base_url must be an externally reachable HTTPS origin or path prefix
without credentials, query, or fragment. TTL must be greater than zero and no more
than 24 hours. Startup fails closed when approval links are enabled without the
durable chat runtime or valid link settings.
Configure Slack Events API to send requests to:
POST https://<ductor-host>/agent-webhook/slackThe endpoint also answers Slack's signed URL-verification challenge. It accepts
app_mention events and direct messages. Bot messages, message subtypes, messages
from the app's own authorized user, and unsupported event types are ignored or
rejected before admission, preventing reply loops.
Connection and identity binding
The webhook does not trust tenant, actor, role, or subscriber identifiers from the
provider body. It resolves the Slack team_id to one active connector connection by
external account id, reloads that connection through the credential boundary, and
verifies the request with the connection's signing_secret (or webhook-secret
credential fallback) and Slack timestamp/signature headers.
Each permitted Slack user also needs an explicit tenant-owned binding in connection metadata. Keys are provider user ids, never display names:
agent_identity_bindings:
U08ABC123:
actor_id: user_01JZ7B9A4X
subscriber_id: customer_42
role: memberactor_id is required. subscriber_id is optional correlation data. An omitted role
defaults to member. Missing, malformed, cross-connection, or unverified bindings
fail before conversation state changes.
Webhook authentication is not actor authorization
A valid Slack signature proves which connected workspace sent the event. The explicit provider-user binding decides which Ductor actor and role may operate the agent. Both checks are required.
Durable ordered admission
One Slack workspace/channel/thread tuple maps to one durable Ductor agent session. The first message creates the mapping; later messages reopen the same session and are processed FIFO. Admission returns quickly after persistence:
{
"ok": true,
"disposition": "accepted",
"queue_position": 0
}Disposition is accepted, duplicate, or queued. Provider event id plus canonical
event hash make exact webhook retries idempotent; the same event id with different
content is a conflict. The encrypted inbound record stores immutable correlation and
message data separately from mutable lease, attempt, wait, and terminal fields.
Current Slack bounds are a 1 MiB webhook body, 32 KiB normalized message, 100 pending
messages per conversation, and 24-hour inbound expiry. Workers use 30-second fenced
leases and at most five processing attempts before dead-lettering. A full backlog
returns HTTP 429 with Retry-After: 30; durable dependency failure returns 503 so
Slack can retry.
Each message becomes one idempotent external turn. Replies execute
slack.send_message through the governed connector registry with the original
connection, channel, and thread_ts. Terminal and approval replies use stable
idempotency keys, so worker restart does not intentionally post duplicates.
Approval links
Slack's current bridge capability declares NativeActions: false. Approval-gated
tools therefore use an encrypted browser link instead of a Slack button. The link
binds tenant, inbound message, agent session, input request, expected journal
sequence, actor, roles, and expiry.
GET /agent-approval?token=<opaque-token> # verify and render only
POST /agent-approval # commit approve or rejectGET never changes agent state. It verifies the token and renders a no-cache,
frame-denied confirmation page. Only POST records a decision. The durable input
sequence fence makes the decision single-use; stale or replayed submissions return
HTTP 409. Expired links return 410, and invalid tokens return 400.
After a valid decision, the waiting inbound message moves from waiting_input back
into ordered processing. The worker responds to the exact pending input request and
continues the existing turn; it does not ask the model to invent a replacement tool
call.
Treat the approval URL as a short-lived bearer secret
Do not log, forward, prefetch, or place the full URL in analytics. Ductor encrypts
its claims, sets Cache-Control: no-store and Referrer-Policy: no-referrer, and
enforces expiry and single use, but possession still authorizes the bound decision
until one of those fences closes it.
Operational checklist
- Create one active Slack connector connection whose external account id is the Slack team id.
- Store the signing secret through connector credentials, never plain application config.
- Add explicit
agent_identity_bindingsfor every permitted Slack user. - Expose
/agent-webhook/slackover HTTPS and register it with Slack Events API. - Enable approval links only with a public HTTPS base URL and the shortest practical TTL.
- Monitor backlog-full, retry, dead-letter, approval-conflict, and connector-action failures without logging message or token content.
- Test duplicate delivery, worker restart, out-of-order arrival, bot-loop rejection, expired approval, double submission, and same-thread reply in staging.
Related
Durable Chat Agent
Dashboard admission, session projections, resumable SSE, inputs, and cancellation.
Durable Agent Runtime
Shared journal states, leases, recovery boundaries, and reconciliation.
Connections
Tenant-owned provider connections and encrypted credential lifecycle.
Agent Tool Security
Tool manifests, schema binding, policy receipts, and approval requirements.
Durable Chat Agent
Admit durable operator turns, stream committed events with a replay cursor, and resume approvals without restarting model or tool work.
Agent Work Runtime
The journal-backed state machine shared by chat and workflow agents, including immutable admission, replay, approvals, execution bounds, and recovery boundaries.