Skip to content

feat(mattermost): add native slash command support (refresh)#32467

Merged
mukhtharcm merged 31 commits intoopenclaw:mainfrom
mukhtharcm:refresh/mattermost-slash-16515
Mar 3, 2026
Merged

feat(mattermost): add native slash command support (refresh)#32467
mukhtharcm merged 31 commits intoopenclaw:mainfrom
mukhtharcm:refresh/mattermost-slash-16515

Conversation

@mukhtharcm
Copy link
Copy Markdown
Member

Summary

Supersedes #18657 due to missing push permission on the original PR head repository.

This branch contains the prepared follow-up fixes:

  • Mattermost slash command test coverage additions
  • Mattermost callback allowlist/callback URL docs clarification
  • Changelog/docs updates for the behavior and deployment caveats

Validation

  • pnpm lint
  • pnpm build
  • pnpm test

Refs #16515

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 3, 2026

🤖 We're reviewing this PR with Aisle

We're running a security check on the changes in this PR now. This usually takes a few minutes. ⌛
We'll post the results here as soon as they're ready.

Progress:

  • Analysis
  • Triage
  • Finalization

Latest run failed. Keeping previous successful results. Trace ID: 019cb1b4380a06a42ed645f5dc50eb55.

Last updated on: 2026-03-03T03:18:35Z

Latest run failed. Keeping previous successful results. Trace ID: 019cb211ea133b63f02190833e8aaf7e.

Last updated on: 2026-03-03T05:00:55Z

Latest run failed. Keeping previous successful results. Trace ID: 019cb2125d40a249b869b975bd3d31c8.

Last updated on: 2026-03-03T05:08:25Z

Latest run failed. Keeping previous successful results. Trace ID: 019cb231d6e4003f822a62f4b598b55d.

Last updated on: 2026-03-03T05:41:18Z

Latest run failed. Keeping previous successful results. Trace ID: 019cb25f038abc19ea64274f67d98bf3.

Last updated on: 2026-03-03T06:27:38Z

Latest run failed. Keeping previous successful results. Trace ID: 019cb25f7fe1cf6902e3b8a2f1e3ad19.

Last updated on: 2026-03-03T06:32:52Z

Latest run failed. Keeping previous successful results. Trace ID: 019cb28631e6e95f08a41307dfd820c2.

Last updated on: 2026-03-03T07:07:55Z

@openclaw-barnacle openclaw-barnacle bot added docs Improvements or additions to documentation channel: mattermost Channel integration: mattermost gateway Gateway runtime size: XL maintainer Maintainer-authored PR labels Mar 3, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 3, 2026

Greptile Summary

This PR adds opt-in native Mattermost slash command support: on connection, the bot registers oc_* commands with the Mattermost API for each team, receives POST callbacks on a dedicated gateway HTTP endpoint, validates the per-command token, and routes through the standard reply pipeline. A cleanup pass deregisters managed commands on shutdown.

Key changes:

  • Three new files (slash-commands.ts, slash-http.ts, slash-state.ts) implement registration, HTTP handling, and per-account shared state respectively.
  • server-http.ts exempts the Mattermost callback path(s) from gateway bearer-token auth so Mattermost can POST without credentials; bypass is correctly scoped to /api/channels/mattermost/ paths only.
  • command-auth.ts fixes override semantics: when commands.allowFrom is configured for specific providers, unlisted providers are now denied (return []) rather than silently falling back to the channel-level allowlist (return null). This is a subtle but intentional behavioral change.
  • Multi-account setups are handled: a single HTTP route dispatches by peeking at the inbound token to find the matching account state, with ambiguous-token detection returning a 409.
  • Two style issues noted: (1) per-request Set construction in server-http.ts repeats config-scanning work on every /api/channels/ request; (2) the multi-account body-buffering path in slash-state.ts buffers up to 64 KB before the method check, unlike the single-account path which checks first.

Confidence Score: 4/5

  • Safe to merge with minor style improvements; no security or correctness issues found.
  • The feature is well-scoped with solid test coverage across all three new modules. The security-sensitive auth bypass in server-http.ts is correctly constrained to Mattermost-specific paths. Token validation is fail-closed throughout. Two style-level inefficiencies (per-request Set construction and unnecessary body buffering before method check) reduce the score from 5 but do not affect correctness or security.
  • src/gateway/server-http.ts (per-request Set construction should be cached) and extensions/mattermost/src/mattermost/slash-state.ts (method guard should precede body buffering in multi-account path).

Last reviewed commit: e7e5782

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

22 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 503 to 564
// Non-channel plugin routes remain plugin-owned and must enforce
// their own auth when exposing sensitive functionality.
if (requestPath.startsWith("/api/channels/")) {
const mattermostCallbackPaths = new Set<string>();
const defaultMmCallbackPath = "/api/channels/mattermost/command";

const isMattermostCommandCallbackPath = (path: string): boolean =>
path === defaultMmCallbackPath || path.startsWith("/api/channels/mattermost/");

const normalizeCallbackPath = (value: unknown): string => {
const trimmed = typeof value === "string" ? value.trim() : "";
if (!trimmed) {
return defaultMmCallbackPath;
}
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
};

const addMattermostCallbackPathIfValid = (path: string) => {
if (isMattermostCommandCallbackPath(path)) {
mattermostCallbackPaths.add(path);
}
};

const tryAddCallbackUrlPath = (rawUrl: unknown) => {
if (typeof rawUrl !== "string") {
return;
}
const trimmed = rawUrl.trim();
if (!trimmed) {
return;
}
try {
const pathname = new URL(trimmed).pathname;
if (pathname) {
addMattermostCallbackPathIfValid(pathname);
}
} catch {
// ignore
}
};

const mmRaw = configSnapshot.channels?.mattermost as Record<string, unknown> | undefined;
const addMmCommands = (raw: unknown) => {
if (raw == null || typeof raw !== "object") {
return;
}
const commands = raw as Record<string, unknown>;
addMattermostCallbackPathIfValid(normalizeCallbackPath(commands?.callbackPath));
tryAddCallbackUrlPath(commands?.callbackUrl);
};

addMmCommands(mmRaw?.commands);
const accountsRaw = (mmRaw?.accounts ?? {}) as Record<string, unknown>;
for (const accountId of Object.keys(accountsRaw)) {
const accountCfg = accountsRaw[accountId] as Record<string, unknown> | undefined;
addMmCommands(accountCfg?.commands);
}

const isMattermostSlashCommandCallback = mattermostCallbackPaths.has(requestPath);
if (requestPath.startsWith("/api/channels/") && !isMattermostSlashCommandCallback) {
const token = getBearerToken(req);
const authResult = await authorizeGatewayConnect({
auth: resolvedAuth,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-request Set construction on every channel request

The Mattermost callback path Set, all associated helper closures (isMattermostCommandCallbackPath, normalizeCallbackPath, addMattermostCallbackPathIfValid, tryAddCallbackUrlPath, addMmCommands), and the URL parsing for callbackUrl entries are all re-created on every incoming HTTP request that hits the /api/channels/ branch. Since configSnapshot rarely changes between requests, this work is redundant.

Consider computing the bypass-path set once when the handler is initialised (or on config change) and reusing it across requests:

// Outside the per-request handler, computed once from configSnapshot:
const mattermostCallbackPaths = buildMattermostCallbackPaths(configSnapshot);

// Then inside the handler, a simple O(1) lookup:
const isMattermostSlashCommandCallback = mattermostCallbackPaths.has(requestPath);

For most deployments the overhead per request is just a few µs, but it's unnecessary given the config is stable between requests and the pattern is inconsistent with how other auth-bypass decisions are made in this file.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-http.ts
Line: 503-564

Comment:
**Per-request Set construction on every channel request**

The Mattermost callback path `Set`, all associated helper closures (`isMattermostCommandCallbackPath`, `normalizeCallbackPath`, `addMattermostCallbackPathIfValid`, `tryAddCallbackUrlPath`, `addMmCommands`), and the URL parsing for `callbackUrl` entries are all re-created on every incoming HTTP request that hits the `/api/channels/` branch. Since `configSnapshot` rarely changes between requests, this work is redundant.

Consider computing the bypass-path set once when the handler is initialised (or on config change) and reusing it across requests:

```ts
// Outside the per-request handler, computed once from configSnapshot:
const mattermostCallbackPaths = buildMattermostCallbackPaths(configSnapshot);

// Then inside the handler, a simple O(1) lookup:
const isMattermostSlashCommandCallback = mattermostCallbackPaths.has(requestPath);
```

For most deployments the overhead per request is just a few µs, but it's unnecessary given the config is stable between requests and the pattern is inconsistent with how other auth-bypass decisions are made in this file.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 3, 2026

Additional Comments (1)

extensions/mattermost/src/mattermost/slash-state.ts
Body buffered before method check in multi-account path

In the single-account fast-path the request is forwarded directly to the handler, which correctly short-circuits on non-POST methods before reading the body. In the multi-account branch, the full body is unconditionally buffered first — then the handler (which is called after token routing) will check req.method and return 405.

For non-POST requests (e.g. health probes or Mattermost's GET reachability check) this means up to 64 KB is buffered unnecessarily. Adding an early method guard before the body-buffering loop would align the behaviour with the single-account path:

// Multi-account: buffer the body, find the matching account by token,
// then replay the request to the correct handler.
if (req.method !== "POST") {
  res.statusCode = 405;
  res.setHeader("Allow", "POST");
  res.end("Method Not Allowed");
  return;
}

const chunks: Buffer[] = [];
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/mattermost/src/mattermost/slash-state.ts
Line: 2270-2286

Comment:
**Body buffered before method check in multi-account path**

In the single-account fast-path the request is forwarded directly to the handler, which correctly short-circuits on non-POST methods before reading the body. In the multi-account branch, the full body is unconditionally buffered first — then the handler (which is called after token routing) will check `req.method` and return 405.

For non-POST requests (e.g. health probes or Mattermost's GET reachability check) this means up to 64 KB is buffered unnecessarily. Adding an early method guard before the body-buffering loop would align the behaviour with the single-account path:

```ts
// Multi-account: buffer the body, find the matching account by token,
// then replay the request to the correct handler.
if (req.method !== "POST") {
  res.statusCode = 405;
  res.setHeader("Allow", "POST");
  res.end("Method Not Allowed");
  return;
}

const chunks: Buffer[] = [];
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7e57820e8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

throw err;
}

const existingByTrigger = new Map(existing.map((cmd) => [cmd.trigger, cmd]));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict slash-command reconciliation to owned commands

registerSlashCommands indexes existing team commands by trigger alone and then updates/deletes the matched entry without checking ownership. In teams that already have another integration using the same trigger (for example /oc_status), startup can repoint or remove that foreign command, breaking the other integration and permanently changing its callback. Reconciliation should only mutate commands that OpenClaw created (for example by validating creator_id or another ownership marker) and treat third-party trigger collisions as conflicts.

Useful? React with 👍 / 👎.

commands: allRegistered,
log: (msg) => runtime.log?.(msg),
});
throw err;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid aborting all teams when one slash registration fails

The catch block rethrows team-level registration errors, which makes setup all-or-nothing across all teams: if one team fails (such as missing permissions), previously successful registrations are cleaned up and slash commands are never activated anywhere. In multi-team deployments this causes a full outage even when some teams are valid; failures should be isolated per team so healthy teams remain usable.

Useful? React with 👍 / 👎.

@mukhtharcm mukhtharcm force-pushed the refresh/mattermost-slash-16515 branch 2 times, most recently from 5c4e5d5 to 785ed9d Compare March 3, 2026 05:01
mukhtharcm added a commit to mukhtharcm/clawdbot that referenced this pull request Mar 3, 2026
mukhtharcm added a commit to mukhtharcm/clawdbot that referenced this pull request Mar 3, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 785ed9d047

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +110 to +112
return allowFrom.some(
(entry) => entry === normalizedId || (normalizedName && entry === normalizedName),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor dangerousNameMatching flag in slash allowlist checks

isSenderAllowed always treats senderName as an auth candidate, so slash callbacks can be authorized by mutable usernames even when channels.mattermost.dangerouslyAllowNameMatching is left at its safer default (false). In practice, if an operator uses username-style allowlist entries for DMs/groups, /oc_* slash commands will be accepted on name match paths that the regular Mattermost monitor intentionally gates behind the dangerous-name flag, which weakens the expected ID-based access control.

Useful? React with 👍 / 👎.

Comment on lines +195 to +199
const effectiveAllowFrom = Array.from(new Set([...configAllowFrom, ...storeAllowFrom]));
const effectiveGroupAllowFrom = Array.from(
new Set([
...(configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom),
...storeAllowFrom,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude DM pairing approvals from group slash command auth

This merges storeAllowFrom (DM pairing approvals) into effectiveGroupAllowFrom, and that list is then used by the slash-command authorization gate, so a user paired in DM can become authorized to run group /oc_* commands without being in configured groupAllowFrom. The shared access-control helper explicitly documents that group command authorization must not inherit DM pairing-store approvals, so this introduces a privilege expansion for native slash commands in group/channel contexts.

Useful? React with 👍 / 👎.

mukhtharcm added a commit to mukhtharcm/clawdbot that referenced this pull request Mar 3, 2026
@mukhtharcm mukhtharcm force-pushed the refresh/mattermost-slash-16515 branch from 785ed9d to 0522e85 Compare March 3, 2026 05:33
mukhtharcm added a commit to mukhtharcm/clawdbot that referenced this pull request Mar 3, 2026
@mukhtharcm mukhtharcm force-pushed the refresh/mattermost-slash-16515 branch from 0522e85 to f028b77 Compare March 3, 2026 05:35
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f028b7762c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".