feat(cli): add slashCommands.disabled setting to gate slash commands (#3445)

* feat(cli): add slashCommands.disabled setting to gate slash commands

Introduces a first-class way for operators to hide and refuse to execute
specific slash commands. Useful for multi-tenant / enterprise / sandboxed
deployments where different users should see different command subsets.

The denylist is sourced from three unioned inputs:

  * `slashCommands.disabled` settings key (string[], UNION merge), so
    workspace scopes can only add to a denylist set at user or system
    scope, never shrink it — matching the shape already used by
    `permissions.deny`.
  * `--disabled-slash-commands` CLI flag (comma-separated or repeated).
  * `QWEN_DISABLED_SLASH_COMMANDS` environment variable.

Matching is case-insensitive against the final (post-rename) command
name, so extension commands are addressable by their disambiguated
form (e.g. `firebase.deploy`). Disabled commands are removed from
`CommandService`'s output, so they disappear from autocomplete and
produce the standard unknown-command path in both interactive TUI and
non-interactive (`--prompt`) modes.

The scope of this change is slash commands only: it does not affect
tool permissions (still `permissions.deny`) or keyboard shortcuts.

* chore(cli): regenerate settings.schema.json for slashCommands.disabled

Regenerates the companion JSON schema consumed by the VS Code extension
after adding the `slashCommands.disabled` entry to the TS schema in the
previous commit. Required by the "Check settings schema is up-to-date"
CI lint step.

* fix(cli): route disabled slash commands to unsupported, not no_command

handleSlashCommand was passing the disabled denylist straight into
CommandService.create, so disabled commands disappeared from
`allCommands` too. The fallback existence check that distinguishes
"known but not allowed in non-interactive mode" from "truly unknown"
then failed, and disabled commands like `/help` fell through to
`no_command` — causing the caller to forward them to the model as
plain prompt text.

Keep `allCommands` unfiltered and apply the denylist only when
constructing the executable set and when producing the unsupported
response. A disabled command now returns `unsupported` with a
"disabled by the current configuration" reason and never reaches the
model. Added three regression tests covering the primary case,
case-insensitive match, and the preserved no_command path for
genuinely unknown input.
This commit is contained in:
ihubanov 2026-04-20 06:06:26 +03:00 committed by GitHub
parent 7cded6e0df
commit 0b8b3da836
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 398 additions and 41 deletions

View file

@ -1151,6 +1151,36 @@ const SETTINGS_SCHEMA = {
},
},
slashCommands: {
type: 'object',
label: 'Slash Commands',
category: 'Advanced',
requiresRestart: true,
default: {},
description:
'Configuration for slash commands exposed by the CLI. Useful for ' +
'locking down the command surface in multi-tenant or enterprise ' +
'deployments.',
showInDialog: false,
properties: {
disabled: {
type: 'array',
label: 'Disabled Slash Commands',
category: 'Advanced',
requiresRestart: true,
default: undefined as string[] | undefined,
description:
'Slash command names to hide and refuse to execute. Matched ' +
'case-insensitively against the final command name (for extension ' +
'commands this is the disambiguated form, e.g. "myext.deploy"). ' +
'Merged as a union across settings scopes, so workspace settings ' +
'can add to but not remove entries defined in system/user settings.',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
},
},
},
tools: {
type: 'object',
label: 'Tools',