Skip to content

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

Closed
echo931 wants to merge 25 commits intoopenclaw:mainfrom
0mg-cc:refresh/mattermost-slash-16515
Closed

feat(mattermost): add native slash command support (refresh)#18657
echo931 wants to merge 25 commits intoopenclaw:mainfrom
0mg-cc:refresh/mattermost-slash-16515

Conversation

@echo931
Copy link
Contributor

@echo931 echo931 commented Feb 16, 2026

Summary

Refresh of stale PR #17302 onto current main.

Reintroduces native Mattermost slash command support, including follow-up hardening commits from the original branch.

Included

  • native slash command registration/state + callback handling
  • per-account command config merge
  • callback URL/path normalization and runtime host/port handling
  • fail-closed behavior and auth-bypass tightening for configured callback paths
  • lifecycle cleanup/race fixes and related robustness updates

Validation

  • pnpm exec vitest run src/auto-reply/command-control.test.ts src/gateway/server-http.hooks-request-timeout.test.ts extensions/mattermost/src/channel.test.ts extensions/mattermost/src/mattermost/client.test.ts

Refs #16515

Greptile Summary

Reintroduces native Mattermost slash command support with comprehensive security hardening:

  • Slash command infrastructure: Registers custom slash commands via Mattermost REST API (/api/v4/commands) and handles callbacks through dedicated HTTP endpoints
  • Command registration lifecycle: Auto-registers commands on bot startup (with per-team registration), tracks state per-account, and cleans up on shutdown to prevent stranded registrations
  • Security hardening: Fail-closed token validation (rejects when no tokens registered), HTTP route whitelisting for Mattermost callbacks to prevent auth bypass, wildcard bind host detection, and improved commands.allowFrom override semantics
  • Multi-account support: Per-account state management with token-based routing for concurrent account deployments
  • Configuration: Opt-in via channels.mattermost.commands.native=true, supports callback URL overrides for reverse proxies, and skill-based command registration via nativeSkills
  • Authorization flow: Enforces same DM/group policies and allowlist checks as websocket messages, with proper command-specific authorization

Confidence Score: 4/5

  • Safe to merge with minor verification recommended for deployment-specific callback URL configuration
  • Score reflects well-architected implementation with robust security measures (fail-closed token validation, HTTP route whitelisting, proper authorization), comprehensive lifecycle management (startup registration, shutdown cleanup, race condition handling), and appropriate test coverage. One point deducted for the localhost callback URL warning being advisory-only rather than failing explicitly when callback URLs would be unreachable
  • No files require special attention - the security hardening (src/gateway/server-http.ts:505-551) properly exempts Mattermost callbacks from gateway auth while maintaining token-based authentication, and the fail-closed behaviors throughout are appropriate

Last reviewed commit: 31eff97

Echo added 19 commits February 16, 2026 18:19
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
Contributor Author

echo931 commented Feb 16, 2026

@codex review

@openclaw-barnacle openclaw-barnacle bot added channel: mattermost Channel integration: mattermost gateway Gateway runtime size: XL labels Feb 16, 2026
Copy link

@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: 31eff977c5

ℹ️ 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".

Comment on lines +1170 to +1174
void cleanupSlashCommands({
client,
commands,
log: (msg) => runtime.log?.(msg),
}).catch((err) => runtime.error?.(`mattermost: slash cleanup failed: ${String(err)}`));

Choose a reason for hiding this comment

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

P1 Badge Await slash cleanup before allowing account restart

Launching cleanupSlashCommands as fire-and-forget on abort can delete commands that a new monitor instance has already adopted. During a quick restart/reload, the new process can list the still-existing commands, mark them as reused, and activate tokens, then the old async cleanup finishes and deletes those same command IDs, leaving native slash commands silently unregistered until another restart.

Useful? React with 👍 / 👎.

Comment on lines +232 to +236
for (const [, state] of accountStates) {
if (state.commandTokens.has(token) && state.handler) {
matchedHandler = state.handler;
break;
}

Choose a reason for hiding this comment

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

P2 Badge Disambiguate token routing across Mattermost accounts

The multi-account router picks the first account whose token set contains the inbound token, but tokens are not guaranteed to be unique across account states (for example when two account configs reuse the same team commands). In that case callbacks are always dispatched to whichever state was iterated first, so commands can run under the wrong account/agent context and the other account never receives its callbacks.

Useful? React with 👍 / 👎.

@echo931
Copy link
Contributor Author

echo931 commented Feb 17, 2026

Addressed in latest push (64d6ce073).

For the routing ambiguity called out in #18657:

  • Added resolveSlashHandlerForToken() to detect none | single | ambiguous matches.
  • Multi-account callback routing now fails closed when a token matches multiple accounts (HTTP 409) instead of dispatching to the first match.
  • Added warning log with involved account IDs to make collisions visible.
  • Added regression tests in slash-state.test.ts for single-match and ambiguous-token scenarios.

@codex review

@chatgpt-codex-connector
Copy link

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

ℹ️ 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
Contributor Author

echo931 commented Feb 18, 2026

@codex review the last refresh (merging main)

Copy link

@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: 7be68ecfc5

ℹ️ 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".

const envPortRaw = process.env.OPENCLAW_GATEWAY_PORT?.trim();
const envPort = envPortRaw ? Number.parseInt(envPortRaw, 10) : NaN;
const gatewayPort =
Number.isFinite(envPort) && envPort > 0 ? envPort : (cfg.gateway?.port ?? 3015);

Choose a reason for hiding this comment

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

P2 Badge Derive callback URL with the current gateway default port

This fallback still hardcodes 3015, but the gateway default runtime port is now 18789; when OPENCLAW_GATEWAY_PORT is absent and gateway.port is unset, slash commands are registered against the wrong callback port and Mattermost callbacks will fail to reach the server. This breaks native slash commands in boot paths that don't inject the env var (for example direct monitor invocations or test harnesses).

Useful? React with 👍 / 👎.

Comment on lines +551 to +552
const isMattermostSlashCommandCallback = mattermostCallbackPaths.has(requestPath);
if (requestPath.startsWith("/api/channels/") && !isMattermostSlashCommandCallback) {

Choose a reason for hiding this comment

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

P1 Badge Scope channel auth bypass to actual Mattermost callback routes

Gateway auth is skipped for any /api/channels/* request whose path matches a value from Mattermost callback config, but this check does not verify that the matched path is actually handled by the Mattermost slash route. If channels.mattermost.commands.callbackPath is misconfigured to another channel endpoint, that endpoint will become publicly reachable without gateway auth.

Useful? React with 👍 / 👎.

@echo931
Copy link
Contributor Author

echo931 commented Feb 18, 2026

Addressed the Codex feedback in commit f8e1f1a:

  • switched slash callback fallback port from 3015 -> 18789 when env+config are absent
  • hardened gateway auth bypass so only Mattermost callback routes under /api/channels/mattermost/* can skip gateway auth
  • added gateway auth regression tests for valid callback bypass + misconfigured callbackPath

Re-running CI now on the updated head. @codex

@chatgpt-codex-connector
Copy link

To use Codex here, create an environment for this repo.

1 similar comment
@chatgpt-codex-connector
Copy link

To use Codex here, create an environment for this repo.

Copy link

@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: fd7e0d28cb

ℹ️ 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".

Comment on lines +151 to +155
: channelInfo
? channelType?.toUpperCase() === "G"
? "group"
: "channel"
: "channel";

Choose a reason for hiding this comment

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

P2 Badge Fail closed when slash channel lookup cannot determine chat type

When fetchMattermostChannel fails, this code falls back to treating the invocation as channel instead of rejecting the request, so DM callbacks no longer go through dmPolicy enforcement. In a DM where lookup fails (for example transient API failure or permission issues), settings like dmPolicy: "disabled" can be bypassed if group policy allows commands, and the inverse misclassification can also incorrectly block valid DMs; returning a deny/temporary error when channel type is unknown would avoid policy drift.

Useful? React with 👍 / 👎.

@echo931
Copy link
Contributor Author

echo931 commented Feb 19, 2026

Addressed in latest push ().\n\n- slash callback auth now fails closed when channel lookup cannot determine the channel type\n- returns ephemeral temporary error instead of defaulting to channel policy\n- logs lookup failure for diagnostics\n\nThis prevents DM/group policy drift when the channel lookup fails transiently.

@echo931
Copy link
Contributor Author

echo931 commented Feb 19, 2026

Correction: commit hash is b6cc649dd.

Copy link

@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: b6cc649dd1

ℹ️ 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".

try {
channelInfo = await fetchMattermostChannel(client, channelId);
} catch (err) {
log?.(`mattermost: slash channel lookup failed for ${channelId}: ${String(err)}`);

Choose a reason for hiding this comment

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

P1 Badge Pass logger into slash authorization error path

In authorizeSlashInvocation, the channel lookup catch path calls log?.(...), but this function never defines or receives a log variable. When fetchMattermostChannel fails (for example during transient Mattermost API outages or permission errors), this throws a ReferenceError, causing the callback handler to fail with a 500 instead of returning the intended temporary-deny response; slash commands become unusable exactly when channel lookup is degraded.

Useful? React with 👍 / 👎.

runtime.error?.(`mattermost: slash cleanup failed: ${String(err)}`);
});
};
opts.abortSignal?.addEventListener("abort", onAbortCleanup, { once: true });

Choose a reason for hiding this comment

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

P2 Badge Run slash cleanup immediately when abort signal is already set

Shutdown cleanup is only wired via abort event subscription here, but there is no immediate signal.aborted check. If cancellation happens during startup registration (or the monitor starts with an already-aborted signal), the handler never runs, slashShutdownCleanup stays null, and managed commands created earlier in startup are left registered and pointing at a dead callback endpoint until manually reconciled.

Useful? React with 👍 / 👎.

@echo931
Copy link
Contributor Author

echo931 commented Feb 19, 2026

Addressed in latest push (db05211bc).

  • fixed undefined logger path by threading log into authorizeSlashInvocation(...)
  • added immediate abort handling for slash cleanup when AbortSignal is already aborted at registration time
  • cleanup remains idempotent (guarded by existing slashShutdownCleanup)

Local targeted tests pass:

  • extensions/mattermost/src/channel.test.ts
  • extensions/mattermost/src/mattermost/slash-state.test.ts
  • extensions/mattermost/src/mattermost/monitor-websocket.test.ts

@tonydehnke
Copy link
Contributor

👋 Hey! AI-assisted review here (Claude Code) — we're tracking this PR as prospective adopters with a live Mattermost deployment. The architecture looks solid, especially the security posture (fail-closed token validation, HTTP 409 on ambiguous multi-account routing, channel lookup failure handling). Great work on the iterative hardening rounds too.

One observation on test coverage — the two largest new files have no tests:

File Lines Tests
slash-http.ts 670
slash-commands.ts 534
slash-state.ts 312 42 lines ✓
server-http.ts (auth bypass) 57 122 lines ✓

The HTTP handler in particular has a lot of security-sensitive authorization logic (DM/group/channel policy enforcement, allowlist checking, pairing flow). Some high-value test scenarios that could help:

  • Token validation: reject invalid/missing tokens, fail-closed when no tokens registered
  • DM/group/channel policy enforcement: disabled, pairing, allowlist, open paths
  • Body parsing: both application/x-www-form-urlencoded and application/json
  • Command text resolution: oc_ prefix stripping, skill command name mapping

No blockers from our side — just flagging the gap in case it's useful. Looking forward to trying this out when it ships! 🎉

@echo931
Copy link
Contributor Author

echo931 commented Feb 20, 2026

Thanks for the detailed pass — super helpful.\n\nAgreed on the coverage gap in and . I’m going to add a focused test slice before merge, prioritizing:\n- token validation / fail-closed paths\n- DM/group/channel policy gating\n- body parsing ( + JSON)\n- command text normalization ( stripping + skill mapping)\n\nI’ll push that as a follow-up commit in this PR and ping here when it’s in.

@echo931
Copy link
Contributor Author

echo931 commented Feb 20, 2026

Follow-up (formatting fix):

Thanks again for the detailed pass — super helpful.

Agreed on the coverage gap in slash-http.ts and slash-commands.ts. I’m going to add a focused test slice before merge, prioritizing:

  • token validation / fail-closed paths
  • DM/group/channel policy gating
  • body parsing (application/x-www-form-urlencoded + JSON)
  • command text normalization (oc_ prefix stripping + skill mapping)

I’ll push that as a follow-up commit in this PR and ping here when it’s in.

@echo931
Copy link
Contributor Author

echo931 commented Feb 22, 2026

Addressed remaining Mattermost native slash review points with focused tests only.

Added coverage:

  • extensions/mattermost/src/mattermost/slash-http.test.ts
    • fail-closed token validation (commandTokens empty)
    • invalid token rejection path
    • DM policy gating (dmPolicy=disabled)
    • channel/group policy gating (groupPolicy=disabled)
  • extensions/mattermost/src/mattermost/slash-commands.test.ts
    • payload parsing for application/x-www-form-urlencoded
    • payload parsing for application/json
    • malformed payload required-field rejection
    • command text normalization + trigger map/fallback mapping paths

Local test commands/results:

  • pnpm test extensions/mattermost/src/mattermost/slash-commands.test.ts extensions/mattermost/src/mattermost/slash-http.test.ts ✅ (2 files, 9 tests passed)
  • pnpm test extensions/mattermost/src/mattermost ✅ (7 files, 27 tests passed)

@mukhtharcm
Copy link
Member

Superseding this PR because the head branch is not writable from this account.
Replacement PR: #32467
#32467

Includes the follow-up Mattermost test + docs/changelog updates.

@mukhtharcm mukhtharcm closed this Mar 3, 2026
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.

3 participants