mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 19:52:02 +00:00
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:
parent
7cded6e0df
commit
0b8b3da836
14 changed files with 398 additions and 41 deletions
|
|
@ -160,6 +160,7 @@ export interface CliArgs {
|
|||
maxSessionTurns: number | undefined;
|
||||
coreTools: string[] | undefined;
|
||||
excludeTools: string[] | undefined;
|
||||
disabledSlashCommands: string[] | undefined;
|
||||
authType: string | undefined;
|
||||
channel: string | undefined;
|
||||
jsonFd?: number | undefined;
|
||||
|
|
@ -530,6 +531,17 @@ export async function parseArguments(): Promise<CliArgs> {
|
|||
coerce: (tools: string[]) =>
|
||||
tools.flatMap((tool) => tool.split(',').map((t) => t.trim())),
|
||||
})
|
||||
.option('disabled-slash-commands', {
|
||||
type: 'array',
|
||||
string: true,
|
||||
description:
|
||||
'Slash command names to hide/disable (comma-separated or ' +
|
||||
'repeated). Merged with the `slashCommands.disabled` setting ' +
|
||||
'and QWEN_DISABLED_SLASH_COMMANDS. Matched case-insensitively ' +
|
||||
'against the final command name.',
|
||||
coerce: (names: string[]) =>
|
||||
names.flatMap((n) => n.split(',').map((t) => t.trim())),
|
||||
})
|
||||
.option('auth-type', {
|
||||
type: 'string',
|
||||
choices: [
|
||||
|
|
@ -926,6 +938,29 @@ export async function loadCliConfig(
|
|||
if (t && !mergedDeny.includes(t)) mergedDeny.push(t);
|
||||
}
|
||||
|
||||
// Merge the slash-command denylist from settings + CLI flag + env var.
|
||||
// Settings merge (UNION across scopes) is already handled upstream; we
|
||||
// only de-duplicate while preserving case for diagnostic purposes.
|
||||
const disabledSlashCommands: string[] = [];
|
||||
const seenDisabled = new Set<string>();
|
||||
const addDisabled = (value: string | undefined) => {
|
||||
if (!value) return;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return;
|
||||
const key = trimmed.toLowerCase();
|
||||
if (!seenDisabled.has(key)) {
|
||||
seenDisabled.add(key);
|
||||
disabledSlashCommands.push(trimmed);
|
||||
}
|
||||
};
|
||||
for (const name of settings.slashCommands?.disabled ?? []) addDisabled(name);
|
||||
for (const name of argv.disabledSlashCommands ?? []) addDisabled(name);
|
||||
for (const name of (process.env['QWEN_DISABLED_SLASH_COMMANDS'] ?? '').split(
|
||||
',',
|
||||
)) {
|
||||
addDisabled(name);
|
||||
}
|
||||
|
||||
// Helper: check if a tool is explicitly covered by an allow rule OR by the
|
||||
// coreTools whitelist. Uses alias matching for coreTools (via isToolEnabled)
|
||||
// to preserve the original behaviour where "ShellTool", "Shell", and
|
||||
|
|
@ -1093,6 +1128,8 @@ export async function loadCliConfig(
|
|||
? argv.allowedTools || undefined
|
||||
: argv.allowedTools || settings.tools?.allowed || undefined,
|
||||
excludeTools: mergedDeny,
|
||||
disabledSlashCommands:
|
||||
disabledSlashCommands.length > 0 ? disabledSlashCommands : undefined,
|
||||
// New unified permissions (PermissionManager source of truth).
|
||||
permissions: {
|
||||
allow: mergedAllow.length > 0 ? mergedAllow : undefined,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue