Skip to content

Commit 245b91b

Browse files
feat(slack): support Enterprise Grid org installs (#102372)
* feat(slack): support Enterprise Grid org installs * docs: refresh generated docs map * fix(slack): satisfy enterprise routing CI * fix(slack): accept org auth without app id * docs(plugin-sdk): document channel policy hooks * fix(slack): reuse canonical sender for enterprise replies * test(slack): fix enterprise sender lint --------- Co-authored-by: Kevin Lin <[email protected]>
1 parent b20b02a commit 245b91b

72 files changed

Lines changed: 3635 additions & 577 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
8c831b037240569085686dfaa1ef8d9b61dfa7a4a570d9ea1bd952e53c4f0044 plugin-sdk-api-baseline.json
2-
4a9a37f95ac8c248f56afa719a7f30bc3e27affad31d934e3a00ecb2bffbb3d9 plugin-sdk-api-baseline.jsonl
1+
d23999706f07d7e3ad75a2d642d9d3df9aa9d9eccfd162e90ee03745af79d788 plugin-sdk-api-baseline.json
2+
dd8db488da6b6a09f1873834fea1b84c95c61ebeba99c347760d767ea6ee0a79 plugin-sdk-api-baseline.jsonl

docs/channels/slack.md

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,219 @@ Relay mode separates Slack ingress from the OpenClaw gateway. A trusted router o
6767

6868
The relay URL must use `wss://` unless it targets localhost. Treat the bearer token and router route table as part of the Slack authorization boundary: routed events enter the normal Slack message handler as authorized activations. A router-provided `slack_identity` in the websocket `hello` frame can set the default outbound username and icon; an explicit identity supplied by the caller still wins. The relay connection reconnects with the same bounded backoff timing as Socket Mode and clears the router-provided identity whenever it disconnects.
6969

70+
### Enterprise Grid org-wide installs
71+
72+
One Slack account can receive messages from every workspace covered by an
73+
Enterprise Grid org-wide installation. Choose direct Socket Mode or HTTP
74+
Request URLs; relay mode is not supported for enterprise accounts. Both
75+
least-privilege manifests below enable only the V1 `message` and `app_mention`
76+
event path, immediate replies, and listener-owned status reactions.
77+
78+
#### Socket Mode
79+
80+
```json
81+
{
82+
"display_information": {
83+
"name": "OpenClaw",
84+
"description": "Slack connector for OpenClaw"
85+
},
86+
"features": {
87+
"bot_user": { "display_name": "OpenClaw", "always_online": true }
88+
},
89+
"oauth_config": {
90+
"scopes": {
91+
"bot": [
92+
"app_mentions:read",
93+
"channels:history",
94+
"channels:read",
95+
"chat:write",
96+
"files:read",
97+
"files:write",
98+
"groups:history",
99+
"groups:read",
100+
"im:history",
101+
"im:read",
102+
"mpim:history",
103+
"mpim:read",
104+
"reactions:write",
105+
"users:read"
106+
]
107+
}
108+
},
109+
"settings": {
110+
"org_deploy_enabled": true,
111+
"socket_mode_enabled": true,
112+
"event_subscriptions": {
113+
"bot_events": [
114+
"app_mention",
115+
"message.channels",
116+
"message.groups",
117+
"message.im",
118+
"message.mpim"
119+
]
120+
}
121+
}
122+
}
123+
```
124+
125+
Have an Enterprise Grid Org Admin or Org Owner approve the app, install it at
126+
the organization level, and choose the workspaces the installation covers.
127+
Confirm that the app is available in every intended workspace before starting
128+
OpenClaw. Generate an app-level token with `connections:write` for Socket Mode,
129+
then copy the bot token from the org installation. Configure the account that
130+
uses the org-installed bot token:
131+
132+
```json5
133+
{
134+
channels: {
135+
slack: {
136+
enabled: true,
137+
mode: "socket",
138+
enterpriseOrgInstall: true,
139+
appToken: { source: "env", provider: "default", id: "SLACK_APP_TOKEN" },
140+
botToken: { source: "env", provider: "default", id: "SLACK_BOT_TOKEN" },
141+
dmPolicy: "open",
142+
allowFrom: ["*"],
143+
groupPolicy: "allowlist",
144+
channels: {
145+
C0123456789: { requireMention: true },
146+
},
147+
},
148+
},
149+
}
150+
```
151+
152+
#### HTTP Request URLs
153+
154+
Use HTTP mode when the Gateway has a public HTTPS endpoint and does not open a
155+
Socket Mode connection. Replace the example URL with the Gateway's public
156+
`webhookPath` URL (default `/slack/events`):
157+
158+
```json
159+
{
160+
"display_information": {
161+
"name": "OpenClaw",
162+
"description": "Slack connector for OpenClaw"
163+
},
164+
"features": {
165+
"bot_user": { "display_name": "OpenClaw", "always_online": true }
166+
},
167+
"oauth_config": {
168+
"scopes": {
169+
"bot": [
170+
"app_mentions:read",
171+
"channels:history",
172+
"channels:read",
173+
"chat:write",
174+
"files:read",
175+
"files:write",
176+
"groups:history",
177+
"groups:read",
178+
"im:history",
179+
"im:read",
180+
"mpim:history",
181+
"mpim:read",
182+
"reactions:write",
183+
"users:read"
184+
]
185+
}
186+
},
187+
"settings": {
188+
"org_deploy_enabled": true,
189+
"event_subscriptions": {
190+
"request_url": "https://gateway-host.example.com/slack/events",
191+
"bot_events": [
192+
"app_mention",
193+
"message.channels",
194+
"message.groups",
195+
"message.im",
196+
"message.mpim"
197+
]
198+
}
199+
}
200+
}
201+
```
202+
203+
Have an Enterprise Grid Org Admin or Org Owner approve the app, install it at
204+
the organization level, and choose the workspaces the installation covers.
205+
After Slack verifies the Request URL, copy the org installation's bot token and
206+
the app's **Basic Information -> App Credentials -> Signing Secret**. Configure
207+
the enterprise account with the same Request URL path:
208+
209+
```json5
210+
{
211+
channels: {
212+
slack: {
213+
enabled: true,
214+
mode: "http",
215+
enterpriseOrgInstall: true,
216+
botToken: { source: "env", provider: "default", id: "SLACK_BOT_TOKEN" },
217+
signingSecret: {
218+
source: "env",
219+
provider: "default",
220+
id: "SLACK_SIGNING_SECRET",
221+
},
222+
webhookPath: "/slack/events",
223+
dmPolicy: "open",
224+
allowFrom: ["*"],
225+
groupPolicy: "allowlist",
226+
channels: {
227+
C0123456789: { requireMention: true },
228+
},
229+
},
230+
},
231+
}
232+
```
233+
234+
At startup, OpenClaw verifies `enterpriseOrgInstall` with Slack `auth.test`.
235+
An org-installed token without the flag, or a workspace token with the flag,
236+
fails startup. Slack remains the source of truth for which workspaces have
237+
granted the installation; OpenClaw then applies the configured channel, user,
238+
DM, and mention policies to each delivered event. Enterprise V1 rejects all
239+
bot-authored `message` and `app_mention` events before dispatch, regardless of
240+
`allowBots`, because org installs do not provide a stable workspace-qualified
241+
bot identity for loop prevention.
242+
243+
Enterprise support is intentionally limited to direct Socket Mode or HTTP
244+
`message` and `app_mention` events and their immediate replies. Relay mode,
245+
slash commands, interactions, App Home, reaction event listeners, pins, Slack
246+
action tools, Slack-native approvals, bindings, queued or scheduled delivery,
247+
and proactive sends are unavailable for an enterprise account. Outbound
248+
acknowledgment, typing, and status reactions are supported through the
249+
listener-owned Slack client and require `reactions:write`; inbound reaction
250+
notifications and reaction action tools remain unavailable.
251+
252+
Immediate replies reuse the standard Slack delivery behavior for chunks,
253+
media, metadata, identity fallback, unfurls, and receipts, but only while the
254+
validated listener-owned client remains in the active event turn. The
255+
in-memory send queue and thread-participation records are partitioned by that
256+
event's workspace; the client itself is never serialized or persisted.
257+
258+
Channel policy keys and `dm.groupChannels` entries must use raw stable Slack channel IDs or the
259+
`channel:<id>` form. OpenClaw normalizes either form to the raw channel ID for
260+
runtime matching; `slack:`, `group:`, and `mpim:` prefixes fail startup.
261+
User policy entries must use stable Slack user IDs; names, slugs, display names,
262+
and email addresses fail startup. IDs must use Slack's canonical uppercase
263+
prefix and body (for example, `C0123456789` or `U0123456789`); lowercase and
264+
short lookalikes fail startup. Enterprise accounts cannot enable
265+
`dangerouslyAllowNameMatching`. Enterprise accounts may set the global
266+
`mentionPatterns.mode`, but `mentionPatterns.allowIn` and
267+
`mentionPatterns.denyIn` fail startup because bare Slack channel IDs are not
268+
workspace-qualified and can be reused across workspaces. Workspace installs
269+
retain the existing scoped mention-pattern behavior. Each accepted workspace
270+
gets separate routing, session, transcript, dedupe, history, and cache identity
271+
even when Slack IDs overlap. Within the `message` stream, ordinary user messages
272+
and user-authored `file_share` events are supported; other message subtypes are
273+
rejected before authorization or system-event handling.
274+
275+
Enterprise DMs must either be disabled (`dm.enabled=false` or
276+
`dmPolicy="disabled"`) or explicitly open with `dmPolicy="open"` and
277+
an effective account `allowFrom` containing the literal `"*"`. An empty
278+
allowlist or user-specific IDs without `"*"` fails startup. Pairing and
279+
per-user DM allowlists are rejected because Slack user IDs are not
280+
workspace-qualified in those authorization stores. Channel and sender policy
281+
continues to apply to channel messages.
282+
70283
## Install
71284

72285
```bash
@@ -77,6 +290,10 @@ openclaw plugins install @openclaw/slack
77290

78291
## Quick setup
79292

293+
The manifests in this section create a workspace-scoped installation. For an
294+
Enterprise Grid organization installation, use the dedicated
295+
[org-wide manifest and workflow](#enterprise-grid-org-wide-installs) instead.
296+
80297
<Tabs>
81298
<Tab title="Socket Mode (default)">
82299
<Steps>
@@ -1242,6 +1459,7 @@ In a channel with `requireMention: true`, include a typed mention of the bot wit
12421459
- text chunks use `channels.slack.textChunkLimit` (default `8000`, capped at Slack's own message-length limit)
12431460
- `channels.slack.streaming.chunkMode="newline"` enables paragraph-first splitting
12441461
- file sends use Slack upload APIs and can include thread replies (`thread_ts`)
1462+
- long file captions use the first Slack-safe text chunk as the upload comment and send remaining chunks as follow-up messages
12451463
- outbound media cap follows `channels.slack.mediaMaxMb` when configured; otherwise channel sends use MIME-kind defaults from media pipeline
12461464

12471465
</Accordion>
@@ -1466,7 +1684,7 @@ Primary reference: [Configuration reference - Slack](/gateway/config-channels#sl
14661684

14671685
<Accordion title="High-signal Slack fields">
14681686

1469-
- mode/auth: `mode`, `botToken`, `appToken`, `signingSecret`, `webhookPath`, `accounts.*`
1687+
- mode/auth: `mode`, `enterpriseOrgInstall`, `botToken`, `appToken`, `signingSecret`, `webhookPath`, `accounts.*`
14701688
- DM access: `dm.enabled`, `dmPolicy`, `allowFrom` (legacy: `dm.policy`, `dm.allowFrom`), `dm.groupEnabled`, `dm.groupChannels`
14711689
- compatibility toggle: `dangerouslyAllowNameMatching` (break-glass; keep off unless needed)
14721690
- channel access: `groupPolicy`, `channels.*`, `channels.*.users`, `channels.*.requireMention`

docs/docs_map.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,9 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
840840
- Headings:
841841
- H2: Choosing a transport
842842
- H3: Relay mode
843+
- H3: Enterprise Grid org-wide installs
844+
- H4: Socket Mode
845+
- H4: HTTP Request URLs
843846
- H2: Install
844847
- H2: Quick setup
845848
- H2: Socket Mode transport tuning
@@ -6974,6 +6977,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
69746977
- H2: Delivery Evidence
69756978
- H2: Existing outbound adapters
69766979
- H2: Durable sends
6980+
- H2: Deferred delivery admission
69776981
- H2: Compatibility dispatch
69786982

69796983
## plugins/sdk-channel-plugins.md
@@ -6987,6 +6991,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
69876991
- H3: Media source params
69886992
- H3: Native payload shaping
69896993
- H3: Session conversation grammar
6994+
- H3: Account-scoped conversation binding support
69906995
- H2: Approvals and channel capabilities
69916996
- H3: Approval auth
69926997
- H3: Payload lifecycle and setup guidance

docs/gateway/config-channels.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,20 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
500500

501501
- **Socket mode** requires both `botToken` and `appToken` (`SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` for default account env fallback).
502502
- **HTTP mode** requires `botToken` plus `signingSecret` (at root or per-account).
503+
- `enterpriseOrgInstall: true` opts an account into the Slack Enterprise Grid
504+
org-wide event path. Startup verifies the bot token with `auth.test` and
505+
fails when the configured mode does not match Slack's installation identity.
506+
Enterprise DMs must be disabled or use `dmPolicy: "open"` with an effective
507+
`allowFrom: ["*"]`. Channel and user policies must use stable Slack IDs;
508+
mutable names and unsupported channel prefixes fail startup. V1 handles only
509+
direct Socket Mode or HTTP `message` and `app_mention` events with immediate
510+
replies; relay, commands, interactions, App Home, reaction event listeners,
511+
pins, action tools, native approvals, bindings, deferred delivery, and
512+
proactive sends are unavailable. Listener-owned acknowledgment, typing, and
513+
status reactions remain available with `reactions:write`; inbound reaction
514+
notifications and reaction action tools are unavailable. See
515+
[Enterprise Grid org-wide installs](/channels/slack#enterprise-grid-org-wide-installs)
516+
for the least-privilege manifest, setup workflow, and complete restrictions.
503517
- `socketMode` passes Slack SDK Socket Mode transport tuning through to the public Bolt receiver API. Use it only when investigating ping/pong timeout or stale websocket behavior. `clientPingTimeout` defaults to `15000`; `serverPingTimeout` and `pingPongLoggingEnabled` are passed only when configured.
504518
- `botToken`, `appToken`, `signingSecret`, and `userToken` accept plaintext
505519
strings or SecretRef objects.

docs/plugins/sdk-channel-outbound.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,28 @@ Use `payloadOutcomes` when a batch mixes sent, suppressed, and failed
117117
payloads. Do not infer hook cancellation from an empty legacy
118118
direct-delivery result.
119119

120+
## Deferred delivery admission
121+
122+
Use `message.durableFinal.admitDeferredDelivery(...)` when a resolved account
123+
cannot safely accept core-managed outbound or deferred delivery. Core calls
124+
this hook synchronously before live outbound work, including paths that skip
125+
queue persistence, and again before replaying a recovered intent. The context
126+
includes `cfg`, `channel`, `to`, `accountId`, and a `phase` of `live` or
127+
`recovery`.
128+
129+
Return `{ status: "allowed" }` to continue. Return
130+
`{ status: "permanent_rejection", reason }` when the delivery must not be
131+
persisted, sent directly, or replayed. A live rejection fails before queue
132+
creation, message hooks, or platform work. A recovery rejection marks the
133+
queued record failed and skips reconciliation and replay. Omitting the hook
134+
means allowed.
135+
136+
The hook is a synchronous admission decision, not a send path. Read only
137+
already-loaded config or runtime state; do not perform network, filesystem, or
138+
other asynchronous I/O. Contract tests should exercise both phases and both
139+
result variants through `ChannelMessageDurableFinalAdapter` from
140+
`openclaw/plugin-sdk/channel-outbound`.
141+
120142
## Compatibility dispatch
121143

122144
Assemble inbound reply dispatch through `dispatchChannelInboundReply(...)`

docs/plugins/sdk-channel-plugins.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,26 @@ normalizes numeric thread ids the same way core does, so prefer it over ad hoc
161161
should expose `messaging.resolveOutboundSessionRoute(...)` so core gets
162162
provider-native session and thread identity without parser shims.
163163

164+
### Account-scoped conversation binding support
165+
166+
Set `conversationBindings.supportsCurrentConversationBinding` when the channel
167+
supports generic current-conversation bindings. `createChatChannelPlugin(...)`
168+
sets this static capability to `true` by default.
169+
170+
If support differs by configured account, also implement
171+
`conversationBindings.isCurrentConversationBindingSupported({ accountId })`.
172+
Core evaluates this synchronous hook only after the static capability is
173+
enabled. Returning `false` makes generic current-conversation capability,
174+
bind, lookup, list, touch, and unbind operations unavailable for that account.
175+
Omitting the hook applies the static capability to every account.
176+
177+
Resolve the answer from already-loaded account config or runtime state. This
178+
hook gates only generic current-conversation bindings; it does not replace
179+
configured binding rules or plugin-owned session routing. Contract tests
180+
should cover at least one supported and one unsupported account through the
181+
`ChannelPlugin["conversationBindings"]` contract exported by
182+
`openclaw/plugin-sdk/channel-core`.
183+
164184
## Approvals and channel capabilities
165185

166186
Most channel plugins do not need approval-specific code. Core owns same-chat

extensions/slack/src/action-runtime.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ describe("handleSlackAction", () => {
3636
} as OpenClawConfig;
3737
}
3838

39+
it("rejects all actions before Slack API work for an enterprise org account", async () => {
40+
await expect(
41+
handleSlackAction(
42+
{ action: "readMessages", channelId: "C123" },
43+
slackConfig({ enterpriseOrgInstall: true }),
44+
),
45+
).rejects.toThrow(/unavailable for Enterprise Grid org installs/);
46+
expect(readSlackMessages).not.toHaveBeenCalled();
47+
});
48+
3949
function createReplyToFirstContext(hasRepliedRef: { value: boolean }) {
4050
return {
4151
currentChannelId: "C123",

extensions/slack/src/action-runtime.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ export async function handleSlackAction(
223223
const accountId = readStringParam(params, "accountId");
224224
const { resolveSlackAccount } = await loadSlackAccountsRuntime();
225225
const account = resolveSlackAccount({ cfg, accountId });
226+
if (account.config.enterpriseOrgInstall === true) {
227+
throw new Error("Slack action tools are unavailable for Enterprise Grid org installs.");
228+
}
226229
const actionConfig = account.actions ?? cfg.channels?.slack?.actions;
227230
const isActionEnabled = createActionGate(actionConfig);
228231
const userToken = account.userToken;

extensions/slack/src/approval-native-gates.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ function isSlackApprovalTransportEnabled(params: {
6464
cfg: OpenClawConfig;
6565
accountId?: string | null;
6666
}): boolean {
67-
return isSlackPluginAccountConfigured(resolveSlackAccount(params));
67+
const account = resolveSlackAccount(params);
68+
return account.config.enterpriseOrgInstall !== true && isSlackPluginAccountConfigured(account);
6869
}
6970

7071
function resolveSlackNativeApprovalConfig(params: {

0 commit comments

Comments
 (0)