Skip to content

Commit b1b41eb

Browse files
authored
feat(mattermost): add native slash command support (refresh) (#32467)
Merged via squash. Prepared head SHA: 9891265 Co-authored-by: mukhtharcm <[email protected]> Co-authored-by: mukhtharcm <[email protected]> Reviewed-by: @mukhtharcm
1 parent 5341b5c commit b1b41eb

20 files changed

Lines changed: 2323 additions & 3 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,8 @@ Docs: https://docs.openclaw.ai
12521252
- iOS/Watch: add an Apple Watch companion MVP with watch inbox UI, watch notification relay handling, and gateway command surfaces for watch status/send flows. (#20054) Thanks @mbelinky.
12531253
- iOS/Gateway: wake disconnected iOS nodes via APNs before `nodes.invoke` and auto-reconnect gateway sessions on silent push wake to reduce invoke failures while the app is backgrounded. (#20332) Thanks @mbelinky.
12541254
- Gateway/CLI: add paired-device hygiene flows with `device.pair.remove`, plus `openclaw devices remove` and guarded `openclaw devices clear --yes [--pending]` commands for removing paired entries and optionally rejecting pending requests. (#20057) Thanks @mbelinky.
1255+
- Mattermost: add opt-in native slash command support with registration lifecycle, callback route/token validation, multi-account token routing, and callback URL/path configuration (`channels.mattermost.commands.*`). (#16515) Thanks @echo931.
1256+
- Mattermost: harden native slash callback auth-bypass behavior for configurable callback paths, add callback validation coverage, and clarify callback reachability/allowlist docs. (#32467) Thanks @mukhtharcm and @echo931.
12551257
- iOS/APNs: add push registration and notification-signing configuration for node delivery. (#20308) Thanks @mbelinky.
12561258
- Gateway/APNs: add a push-test pipeline for APNs delivery validation in gateway flows. (#20307) Thanks @mbelinky.
12571259
- Security/Audit: add `gateway.http.no_auth` findings when `gateway.auth.mode="none"` leaves Gateway HTTP APIs reachable, with loopback warning and remote-exposure critical severity, plus regression coverage and docs updates.

docs/channels/mattermost.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,45 @@ Minimal config:
5555
}
5656
```
5757

58+
## Native slash commands
59+
60+
Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands via
61+
the Mattermost API and receives callback POSTs on the gateway HTTP server.
62+
63+
```json5
64+
{
65+
channels: {
66+
mattermost: {
67+
commands: {
68+
native: true,
69+
nativeSkills: true,
70+
callbackPath: "/api/channels/mattermost/command",
71+
// Use when Mattermost cannot reach the gateway directly (reverse proxy/public URL).
72+
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
73+
},
74+
},
75+
},
76+
}
77+
```
78+
79+
Notes:
80+
81+
- `native: "auto"` defaults to disabled for Mattermost. Set `native: true` to enable.
82+
- If `callbackUrl` is omitted, OpenClaw derives one from gateway host/port + `callbackPath`.
83+
- For multi-account setups, `commands` can be set at the top level or under
84+
`channels.mattermost.accounts.<id>.commands` (account values override top-level fields).
85+
- Command callbacks are validated with per-command tokens and fail closed when token checks fail.
86+
- Reachability requirement: the callback endpoint must be reachable from the Mattermost server.
87+
- Do not set `callbackUrl` to `localhost` unless Mattermost runs on the same host/network namespace as OpenClaw.
88+
- Do not set `callbackUrl` to your Mattermost base URL unless that URL reverse-proxies `/api/channels/mattermost/command` to OpenClaw.
89+
- A quick check is `curl https://<gateway-host>/api/channels/mattermost/command`; a GET should return `405 Method Not Allowed` from OpenClaw, not `404`.
90+
- Mattermost egress allowlist requirement:
91+
- If your callback targets private/tailnet/internal addresses, set Mattermost
92+
`ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain.
93+
- Use host/domain entries, not full URLs.
94+
- Good: `gateway.tailnet-name.ts.net`
95+
- Bad: `https://gateway.tailnet-name.ts.net`
96+
5897
## Environment variables (default account)
5998

6099
Set these on the gateway host if you prefer env vars:

docs/gateway/configuration-reference.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,13 @@ Mattermost ships as a plugin: `openclaw plugins install @openclaw/mattermost`.
443443
dmPolicy: "pairing",
444444
chatmode: "oncall", // oncall | onmessage | onchar
445445
oncharPrefixes: [">", "!"],
446+
commands: {
447+
native: true, // opt-in
448+
nativeSkills: true,
449+
callbackPath: "/api/channels/mattermost/command",
450+
// Optional explicit URL for reverse-proxy/public deployments
451+
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
452+
},
446453
textChunkLimit: 4000,
447454
chunkMode: "length",
448455
},
@@ -452,6 +459,13 @@ Mattermost ships as a plugin: `openclaw plugins install @openclaw/mattermost`.
452459

453460
Chat modes: `oncall` (respond on @-mention, default), `onmessage` (every message), `onchar` (messages starting with trigger prefix).
454461

462+
When Mattermost native commands are enabled:
463+
464+
- `commands.callbackPath` must be a path (for example `/api/channels/mattermost/command`), not a full URL.
465+
- `commands.callbackUrl` must resolve to the OpenClaw gateway endpoint and be reachable from the Mattermost server.
466+
- For private/tailnet/internal callback hosts, Mattermost may require
467+
`ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain.
468+
Use host/domain values, not full URLs.
455469
- `channels.mattermost.configWrites`: allow or deny Mattermost-initiated config writes.
456470
- `channels.mattermost.requireMention`: require `@mention` before replying in channels.
457471
- Optional `channels.mattermost.defaultAccount` overrides default account selection when it matches a configured account id.

extensions/mattermost/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
22
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
33
import { mattermostPlugin } from "./src/channel.js";
4+
import { getSlashCommandState, registerSlashCommandRoute } from "./src/mattermost/slash-state.js";
45
import { setMattermostRuntime } from "./src/runtime.js";
56

67
const plugin = {
@@ -11,6 +12,11 @@ const plugin = {
1112
register(api: OpenClawPluginApi) {
1213
setMattermostRuntime(api.runtime);
1314
api.registerChannel({ plugin: mattermostPlugin });
15+
16+
// Register the HTTP route for slash command callbacks.
17+
// The actual command registration with MM happens in the monitor
18+
// after the bot connects and we know the team ID.
19+
registerSlashCommandRoute(api);
1420
},
1521
};
1622

extensions/mattermost/src/channel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
172172
reactions: true,
173173
threads: true,
174174
media: true,
175+
nativeCommands: true,
175176
},
176177
streaming: {
177178
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },

extensions/mattermost/src/config-schema.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ import {
88
import { z } from "zod";
99
import { buildSecretInputSchema } from "./secret-input.js";
1010

11+
const MattermostSlashCommandsSchema = z
12+
.object({
13+
/** Enable native slash commands. "auto" resolves to false (opt-in). */
14+
native: z.union([z.boolean(), z.literal("auto")]).optional(),
15+
/** Also register skill-based commands. */
16+
nativeSkills: z.union([z.boolean(), z.literal("auto")]).optional(),
17+
/** Path for the callback endpoint on the gateway HTTP server. */
18+
callbackPath: z.string().optional(),
19+
/** Explicit callback URL (e.g. behind reverse proxy). */
20+
callbackUrl: z.string().optional(),
21+
})
22+
.strict()
23+
.optional();
24+
1125
const MattermostAccountSchemaBase = z
1226
.object({
1327
name: z.string().optional(),
@@ -35,6 +49,7 @@ const MattermostAccountSchemaBase = z
3549
reactions: z.boolean().optional(),
3650
})
3751
.optional(),
52+
commands: MattermostSlashCommandsSchema,
3853
})
3954
.strict();
4055

extensions/mattermost/src/mattermost/accounts.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,21 @@ function mergeMattermostAccountConfig(
8383
defaultAccount?: unknown;
8484
};
8585
const account = resolveAccountConfig(cfg, accountId) ?? {};
86-
return { ...base, ...account };
86+
87+
// Shallow merging is fine for most keys, but `commands` should be merged
88+
// so that account-specific overrides (callbackPath/callbackUrl) do not
89+
// accidentally reset global settings like `native: true`.
90+
const mergedCommands = {
91+
...(base.commands ?? {}),
92+
...(account.commands ?? {}),
93+
};
94+
95+
const merged = { ...base, ...account };
96+
if (Object.keys(mergedCommands).length > 0) {
97+
merged.commands = mergedCommands;
98+
}
99+
100+
return merged;
87101
}
88102

89103
function resolveMattermostRequireMention(config: MattermostAccountConfig): boolean | undefined {

extensions/mattermost/src/mattermost/client.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,19 @@ export async function createMattermostPost(
190190
});
191191
}
192192

193+
export type MattermostTeam = {
194+
id: string;
195+
name?: string | null;
196+
display_name?: string | null;
197+
};
198+
199+
export async function fetchMattermostUserTeams(
200+
client: MattermostClient,
201+
userId: string,
202+
): Promise<MattermostTeam[]> {
203+
return await client.request<MattermostTeam[]>(`/users/${userId}/teams`);
204+
}
205+
193206
export async function uploadMattermostFile(
194207
client: MattermostClient,
195208
params: {

0 commit comments

Comments
 (0)