← Back to Plugins
Channels

Slack Action Token

evgeniy270894 By evgeniy270894 👁 31 views ▲ 0 votes

Keep the Slack action_token so an OpenClaw plugin can use Slack's Real-time Search API with a bot token

GitHub

Install

openclaw plugins install ~/plugins/slack-patched

Configuration Example

ctx.app.event("message", async ({ event, body }) => {
  __openclawSlackActionTokenCapture(event, body);   // ← added
  await handleIncomingMessageEvent({ event, body });
});

README

# openclaw-slack-action-token

Teaches [`@openclaw/slack`](https://www.npmjs.com/package/@openclaw/slack) to keep
the Slack `action_token`, so an OpenClaw plugin can call Slack's Real-time Search
API (`assistant.search.context`) with the workspace's **bot** token.

Verified end to end on OpenClaw 2026.6.34 / `@openclaw/slack` 2026.6.34.

## Why this is needed

Slack hands AI apps a short-lived `action_token` inside inbound event payloads.
It is the only way a bot token may search:

> All API calls made using a bot token require an `action_token`.
> — [assistant.search.context](https://docs.slack.dev/reference/methods/assistant.search.context)

The token arrives in `message.im` (a DM needs no mention), and in
`message.channels` / `message.groups` / `message.mpim` / `app_mention` when the
app is mentioned. The stock plugin never reads the field — zero occurrences of
`action_token` in the published 2026.6.34 bundle — so it is gone before any tool
runs, and `assistant.search.context` answers `invalid_action_token`.

The alternative, `search.messages`, is user-token only (`not_allowed_token_type`
for a bot token) and cannot be unlocked with extra scopes.

## What it does

Two call sites gain one line each, and one helper is appended:

```js
ctx.app.event("message", async ({ event, body }) => {
  __openclawSlackActionTokenCapture(event, body);   // ← added
  await handleIncomingMessageEvent({ event, body });
});
```

The helper stores the newest token per `channel:user` on
`globalThis.__openclawSlackActionTokens`. A global rather than a plugin API
because the consumer is a different plugin in the same gateway process, and a
global is the one seam that cannot drift with an SDK change.

Total diff: under 30 lines, in one file. That is deliberate — every line has to
be re-applied to the next release.

## Install

```bash
# 1. copy the installed package (find it with `openclaw plugins list`)
cp -r ~/.openclaw/npm/projects/…/node_modules/@openclaw/slack ~/plugins/slack-patched

# 2. patch the copy (refuses loudly if upstream moved the handlers)
node patch.mjs ~/plugins/slack-patched --check   # dry run
node patch.mjs ~/plugins/slack-patched

# 3. load the patched build instead of the stock one
openclaw plugins install ~/plugins/slack-patched --link
openclaw gateway restart
```

`--link` points at the directory, so re-patching after an upgrade needs no
reinstall. `--force` is rejected together with `--link`; that is expected.

Confirm it is live — the first captured token logs once per process:

```
[slack-action-token] capture active
```

Roll back with `openclaw plugins install @openclaw/slack@<version> --force`.

## Upgrading

The bundle filename carries a content hash, so the patcher finds the file by
content, never by name. If upstream renames or restructures a handler, the
patcher exits non-zero and says so rather than producing a silent no-op: a
quietly unpatched plugin is indistinguishable from "search stopped working".

```bash
node patch.mjs <new copy> --check && node patch.mjs <new copy>
```

If it refuses, re-read `ANCHORS` in `lib/patch.mjs` against the new bundle.

## Consuming the token

`lib/read-token.mjs` is dependency-free; copy it or import it.

```js
import { readActionToken } from "./read-token.mjs";
const token = readActionToken(channelId, userId);   // null when absent or stale
```

**Pick the right person's token.** The token decides *whose* Slack permissions
the search runs under — Slack returns only what that user can see. Handing over
someone else's token silently searches as them. In a DM the session identifies
the user; in a channel it does not, so a consumer should refuse when more than
one person has spoken recently rather than guess. A worked example lives in
[`openclaw-slack-acl`](https://github.com/openclaw/openclaw) — see
`lib/action-token.mjs` there.

## Two things worth knowing before you build on this

**The token is tied to an interaction.** It arrives with a message and expires.
Search works while a person is talking to the bot; scheduled or background
search does not, and no amount of caching fixes that.

**`assistant.search.context` is semantic, not literal.** Measured on a live
workspace: the phrase `маркер для проверки поиска` returned 20 hits, while the
unique codeword sitting in the very same message returned zero. Pass the user's
question in their own words. Tests built on random codewords — the usual way to
prove isolation — will fail here for reasons that have nothing to do with
permissions.

## Tests

```bash
node --test test/patch.test.mjs
```

Covers both call sites being patched, the helper appearing once, refusal on a
missing or duplicated anchor, refusal to double-patch, the capture storing and
ignoring malformed payloads, and staleness.

## Licence

MIT.
channels

Comments

Sign in to leave a comment

Loading comments...