Skip to content

feat(mattermost): add native slash command support#17302

Closed
echo931 wants to merge 19 commits intoopenclaw:mainfrom
0mg-cc:feat/mattermost-native-slash-commands
Closed

feat(mattermost): add native slash command support#17302
echo931 wants to merge 19 commits intoopenclaw:mainfrom
0mg-cc:feat/mattermost-native-slash-commands

Conversation

@echo931
Copy link
Copy Markdown
Contributor

@echo931 echo931 commented Feb 15, 2026

What

Adds opt-in native Mattermost slash command support to OpenClaw, including command registration, callback handling, and account-aware routing.

Why

Mattermost users need first-class slash commands (e.g. /oc_status) without relying on external bridges. This keeps UX consistent with other channels while preserving fail-closed auth/token checks.

Scope

Single theme PR: Mattermost native slash-command support + related hardening needed to safely ship it (auth bypass precision, command registration cleanup, callback URL handling, and labeler fallback robustness required for CI/backfill in this workflow).

Validation

  • pnpm build
  • pnpm check
  • 🔄 pnpm test (full suite in progress during maintainer sweep; no failures observed so far in this run)
  • Plus targeted lint/tests were run on touched files during prior review iterations.

AI assistance transparency

This PR was heavily assisted by LLMs.

Closes #16515.


Previous context from fork: 0mg-cc#10

Greptile Summary

This PR adds opt-in native Mattermost slash command support, enabling OpenClaw to register custom slash commands (e.g., /oc_status, /oc_model) via the Mattermost REST API and handle incoming command callbacks through an HTTP endpoint on the gateway.

Last reviewed commit: f1c9f43

Echo added 18 commits February 14, 2026 16:46
Register custom slash commands via Mattermost REST API at startup,
handle callbacks via HTTP endpoint on the gateway, and clean up
commands on shutdown.

- New modules: slash-commands.ts (API + registration), slash-http.ts
  (callback handler), slash-state.ts (shared state bridge)
- Config schema extended with commands.{native,nativeSkills,callbackPath,callbackUrl}
- Uses oc_ prefix for triggers (oc_status, oc_model, etc.) to avoid
  conflicts with Mattermost built-in commands
- Opt-in via channels.mattermost.commands.native: true
- Capability nativeCommands: true exposed for command registry

Closes openclaw#16515
Address Codex review findings:

1. slash-http.ts: Token validation now rejects when commandTokens set is
   empty (e.g. registration failure). Previously an empty set meant any
   token was accepted — fail-open vulnerability.

2. slash-state.ts: Replaced global singleton with per-account state Map
   keyed by accountId. Multi-account deployments no longer overwrite each
   other's tokens, registered commands, or handlers. The HTTP route
   dispatcher matches inbound tokens to the correct account.

3. monitor.ts: Updated getSlashCommandState/deactivateSlashCommands calls
   to pass accountId.
Address Codex follow-up feedback:

- Enforce DM/group allowlist + control-command gating for native slash commands
  (no unconditional CommandAuthorized=true).
- Register callback HTTP routes for all configured callbackPath values
  (top-level + per-account overrides).
- Track whether slash commands were created by this process; only delete
  managed commands on shutdown, leaving pre-existing commands intact.
- parseSlashCommandPayload JSON branch now validates required fields
  (token, team_id, channel_id, user_id, command) like the form-encoded
  branch, preventing runtime exceptions on malformed JSON payloads
- normalizeCallbackPath() ensures leading '/' to prevent malformed URLs
  like 'http://host:portapi/...' when callbackPath lacks a leading slash
- Applied in resolveSlashCommandConfig and resolveCallbackUrl

Addresses Codex review round 3 (P2 items).
…RL reconciliation

- Reject slash command registration when callbackUrl resolves to
  loopback but Mattermost baseUrl is remote; log actionable error
  directing user to set commands.callbackUrl explicitly
- Honor commands.nativeSkills: when enabled, dynamically list skill
  commands and register them with oc_ prefix alongside built-in commands
- Reconcile existing commands on callback URL change: attempt PUT update,
  fallback to delete+recreate for stale commands with mismatched URLs
- Add updateMattermostCommand() for PUT /api/v4/commands/{id}

Addresses Codex review round 4 (P1 + P2 items).
…ackUrl pathname

- normalizeAllowList/isSenderAllowed in slash-http.ts now matches the
  websocket monitor: strips mattermost:/user:/@  prefixes and supports
  the '*' wildcard, so configs that work for WS also work for slash cmds
- registerSlashCommandRoute extracts pathname from explicit callbackUrl
  and registers it alongside callbackPath, so callbacks hit a registered
  route even when callbackUrl uses a non-default pathname

Addresses Codex review round 5 (P1 + P2).
…rMap for accurate command name resolution

- Export listSkillCommandsForAgents and SkillCommandSpec from plugin-sdk/index.ts
  (removes deep relative import in monitor.ts)
- Add originalName field to MattermostCommandSpec for preserving pre-prefix names
- Build trigger→originalName map in monitor.ts, thread through slash-state → slash-http
- resolveCommandText() now uses triggerMap for accurate name lookup
  (oc_report → /oc_report correctly, not /report)
…nt race

Snapshot registered commands, then deactivate state immediately on abort.
Prevents race where new monitor activates fresh state that gets wiped
by the delayed .then() of the old monitor's cleanup promise.
@echo931
Copy link
Copy Markdown
Contributor Author

echo931 commented Feb 15, 2026

@greptileai

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.

15 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +385 to +396
const addMmCommands = (raw: unknown) => {
const commands = raw as Record<string, unknown> | undefined;
mattermostCallbackPaths.add(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);
}
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.

Default callback path always added to auth bypass set

addMmCommands is called unconditionally at line 391 with mmRaw?.commands. When mattermost has no commands config (the default, since slash commands are opt-in), commands is undefined, so normalizeCallbackPath(undefined?.callbackPath) returns the default path "/api/channels/mattermost/command". This means /api/channels/mattermost/command is always exempt from gateway auth — even when mattermost isn't configured or slash commands are disabled.

The immediate security risk is low (requests to that path fall through to the plugin handler which validates its own command token, and if no handler is registered they get a 404). But as a defense-in-depth measure, the bypass should only apply when slash commands are actually configured. Consider guarding with a check like:

Suggested change
const addMmCommands = (raw: unknown) => {
const commands = raw as Record<string, unknown> | undefined;
mattermostCallbackPaths.add(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 addMmCommands = (raw: unknown) => {
if (raw == null || typeof raw !== "object") return;
const commands = raw as Record<string, unknown>;
mattermostCallbackPaths.add(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);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-http.ts
Line: 385:396

Comment:
**Default callback path always added to auth bypass set**

`addMmCommands` is called unconditionally at line 391 with `mmRaw?.commands`. When mattermost has no `commands` config (the default, since slash commands are opt-in), `commands` is `undefined`, so `normalizeCallbackPath(undefined?.callbackPath)` returns the default path `"/api/channels/mattermost/command"`. This means `/api/channels/mattermost/command` is always exempt from gateway auth — even when mattermost isn't configured or slash commands are disabled.

The immediate security risk is low (requests to that path fall through to the plugin handler which validates its own command token, and if no handler is registered they get a 404). But as a defense-in-depth measure, the bypass should only apply when slash commands are actually configured. Consider guarding with a check like:

```suggestion
        const addMmCommands = (raw: unknown) => {
          if (raw == null || typeof raw !== "object") return;
          const commands = raw as Record<string, unknown>;
          mattermostCallbackPaths.add(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);
        }
```

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: f1c9f43f45

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

id: token
shell: bash
run: |
echo "token=${{ steps.app-token.outputs.token || github.token }}" >> "$GITHUB_OUTPUT"
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 Handle 403 when falling back to GITHUB_TOKEN

This backfill job now falls back to github.token when GH_APP_PRIVATE_KEY is absent, but the contributor-label script in the same job still treats non-404 membership lookup failures as fatal. In that fallback mode, teams.getMembershipForUserInOrg commonly returns 403 for GITHUB_TOKEN, so workflow_dispatch backfills can abort on the first PR instead of completing (unlike the other updated jobs, which already downgrade 403 to a warning).

Useful? React with 👍 / 👎.

Comment on lines +403 to +405
} catch (err) {
runtime.error?.(`mattermost: failed to register slash commands: ${String(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 Clean up partial slash setup when team registration fails

The outer registration block logs and exits on the first thrown error, but by this point earlier teams may already have had commands created. In that partial-success scenario (e.g., one team lacks command permissions), activateSlashCommands is never reached, so callbacks return the “not initialized” 503 response while newly created commands remain stranded because shutdown cleanup reads from slash state that was never activated.

Useful? React with 👍 / 👎.

@echo931
Copy link
Copy Markdown
Contributor Author

echo931 commented Feb 15, 2026

Applied fixes for Greptile+Codex feedback; tests/lint clean.

  • server-http: guard addMmCommands() so we don't add Mattermost default callbackPath to auth bypass when commands config is absent.
  • labeler.yml: treat 403 from teams.getMembershipForUserInOrg as non-fatal (GITHUB_TOKEN often lacks org/team read perms).
  • mattermost monitor: if slash command registration fails mid-loop, cleanup any commands already created to avoid partial/orphaned registrations.

Commit: 1716ff2

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@echo931
Copy link
Copy Markdown
Contributor Author

echo931 commented Feb 16, 2026

Thanks for the maintainer sweep — updated PR description to align with CONTRIBUTING.md:

  • explicit what/why
  • explicit focused scope
  • explicit AI-assistance transparency
  • explicit validation section

Validation status right now:

  • pnpm build
  • pnpm check
  • pnpm test is currently running locally (full suite); I’ll post a follow-up once it completes.

@echo931
Copy link
Copy Markdown
Contributor Author

echo931 commented Feb 16, 2026

Follow-up: full local validation is now complete ✅

  • pnpm build
  • pnpm check
  • pnpm test ✅ (45 files, 270 tests passed)

PR is aligned with CONTRIBUTING.md and ready for maintainer review/merge.

@echo931
Copy link
Copy Markdown
Contributor Author

echo931 commented Feb 16, 2026

Closing this stale branch because it is heavily behind main and no longer up-to-date for review.\n\nI will reopen a fresh PR from current main for this feature/bug once rebased/reimplemented cleanly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: mattermost Channel integration: mattermost gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Native slash command support for Mattermost plugin

1 participant