Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,20 @@ LSP server configuration is done through `.lsp.json` files in your project root

#### security

| Setting | Type | Description | Default |
| ------------------------------ | ------- | ------------------------------------------------- | ----------- |
| `security.folderTrust.enabled` | boolean | Setting to track whether Folder trust is enabled. | `false` |
| `security.auth.selectedType` | string | The currently selected authentication type. | `undefined` |
| `security.auth.enforcedType` | string | The required auth type (useful for enterprises). | `undefined` |
| `security.auth.useExternal` | boolean | Whether to use an external authentication flow. | `undefined` |
| Setting | Type | Description | Default |
| -------------------------------------- | ---------------- | ------------------------------------------------------------------------------ | ----------- |
| `security.folderTrust.enabled` | boolean | Setting to track whether Folder trust is enabled. | `false` |
| `security.auth.selectedType` | string | The currently selected authentication type. | `undefined` |
| `security.auth.enforcedType` | string | The required auth type (useful for enterprises). | `undefined` |
| `security.auth.useExternal` | boolean | Whether to use an external authentication flow. | `undefined` |
| `security.redaction.enabled` | boolean | Enable client-side redaction before provider requests. | `false` |
| `security.redaction.placeholderPrefix` | string | Placeholder prefix (keep `__VG_` for compatibility). | `__VG_` |
| `security.redaction.keywords` | object | Exact substring matches: `{ "secretValue": "CATEGORY" }`. | `{}` |
| `security.redaction.patterns` | object | Regex matches (JavaScript syntax): `{ "regexPattern": "CATEGORY" }`. | `{}` |
| `security.redaction.builtins` | array of strings | Built-in detectors: `email`, `china_phone`, `china_id`, `uuid`, `ipv4`, `mac`. | `[]` |
| `security.redaction.exclude` | array of strings | Exact values that should not be redacted. | `[]` |
| `security.redaction.ttlMinutes` | number | How long placeholder ↔ original mappings are kept in memory. | `60` |
| `security.redaction.maxSize` | number | Maximum number of in-memory mappings to keep. | `10000` |

#### advanced

Expand Down
27 changes: 14 additions & 13 deletions docs/users/features/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,20 @@ Commands specifically for controlling interface and output language.

Commands for managing AI tools and models.

| Command | Description | Usage Examples |
| ---------------- | --------------------------------------------- | --------------------------------------------- |
| `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` |
| `/tools` | Display currently available tool list | `/tools`, `/tools desc` |
| `/skills` | List and run available skills | `/skills`, `/skills <name>` |
| `/approval-mode` | Change approval mode for tool usage | `/approval-mode <mode (auto-edit)> --project` |
| →`plan` | Analysis only, no execution | Secure review |
| →`default` | Require approval for edits | Daily use |
| →`auto-edit` | Automatically approve edits | Trusted environment |
| →`yolo` | Automatically approve all | Quick prototyping |
| `/model` | Switch model used in current session | `/model` |
| `/extensions` | List all active extensions in current session | `/extensions` |
| `/memory` | Manage AI's instruction context | `/memory add Important Info` |
| Command | Description | Usage Examples |
| ---------------- | --------------------------------------------- | ------------------------------------------------------ |
| `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` |
| `/tools` | Display currently available tool list | `/tools`, `/tools desc` |
| `/skills` | List and run available skills | `/skills`, `/skills <name>` |
| `/approval-mode` | Change approval mode for tool usage | `/approval-mode <mode (auto-edit)> --project` |
| →`plan` | Analysis only, no execution | Secure review |
| →`default` | Require approval for edits | Daily use |
| →`auto-edit` | Automatically approve edits | Trusted environment |
| →`yolo` | Automatically approve all | Quick prototyping |
| `/model` | Switch model used in current session | `/model` |
| `/extensions` | List all active extensions in current session | `/extensions` |
| `/memory` | Manage AI's instruction context | `/memory add Important Info` |
| `/vibeguard` | Manage client-side redaction | `/vibeguard status`, `/vibeguard on`, `/vibeguard off` |

### 1.5 Information, Settings, and Help

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ export async function loadCliConfig(
ideMode,
chatCompression: settings.model?.chatCompression,
folderTrust,
redaction: settings.security?.redaction,
interactive,
trustedFolder,
useRipgrep: settings.tools?.useRipgrep,
Expand Down
94 changes: 94 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,100 @@ const SETTINGS_SCHEMA = {
description: 'Security-related settings.',
showInDialog: false,
properties: {
redaction: {
type: 'object',
label: 'Client-side Redaction',
category: 'Security',
requiresRestart: true,
default: {},
description:
'Redact configured secrets/PII into placeholders before sending prompts/tool history to providers.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable Redaction',
category: 'Security',
requiresRestart: true,
default: false,
description:
'Enable client-side redaction before provider requests (default off).',
showInDialog: false,
},
placeholderPrefix: {
type: 'string',
label: 'Placeholder Prefix',
category: 'Security',
requiresRestart: true,
default: '__VG_',
description:
'Placeholder prefix. Keep "__VG_" for compatibility with VibeGuard-style placeholders.',
showInDialog: false,
},
keywords: {
type: 'object',
label: 'Keywords',
category: 'Security',
requiresRestart: true,
default: {} as Record<string, string>,
description:
'Exact substring matches: { "secretValue": "CATEGORY" }',
showInDialog: false,
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
},
patterns: {
type: 'object',
label: 'Patterns',
category: 'Security',
requiresRestart: true,
default: {} as Record<string, string>,
description:
'Regex matches (JavaScript syntax): { "regexPattern": "CATEGORY" }',
showInDialog: false,
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
},
builtins: {
type: 'array',
label: 'Built-in Detectors',
category: 'Security',
requiresRestart: true,
default: [] as string[],
description:
'Built-in patterns: email, china_phone, china_id, uuid, ipv4, mac',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
},
exclude: {
type: 'array',
label: 'Exclude',
category: 'Security',
requiresRestart: true,
default: [] as string[],
description: 'Exact values that should not be redacted.',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
},
ttlMinutes: {
type: 'number',
label: 'Mapping TTL (minutes)',
category: 'Security',
requiresRestart: true,
default: 60,
description:
'How long placeholder ↔ original mappings are kept in memory.',
showInDialog: false,
},
maxSize: {
type: 'number',
label: 'Max Mapping Size',
category: 'Security',
requiresRestart: true,
default: 10000,
description: 'Maximum number of in-memory mappings to keep.',
showInDialog: false,
},
},
},
folderTrust: {
type: 'object',
label: 'Folder Trust',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { toolsCommand } from '../ui/commands/toolsCommand.js';
import { vimCommand } from '../ui/commands/vimCommand.js';
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
import { insightCommand } from '../ui/commands/insightCommand.js';
import { vibeguardCommand } from '../ui/commands/vibeguardCommand.js';

/**
* Loads the core, hard-coded slash commands that are an integral part
Expand Down Expand Up @@ -89,6 +90,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
toolsCommand,
settingsCommand,
vimCommand,
vibeguardCommand,
setupGithubCommand,
terminalSetupCommand,
insightCommand,
Expand Down
87 changes: 87 additions & 0 deletions packages/cli/src/ui/commands/vibeguardCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/

import type { MessageActionReturn, SlashCommand } from './types.js';
import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';

function formatStatusMessage(stats: {
enabled: boolean;
mappings: number;
prefix: string;
}): string {
const enabledLine = `Enabled (this session): ${stats.enabled ? 'true' : 'false'}`;
const mappingsLine = `Mappings (in-memory): ${stats.mappings}`;
const formatLine = `Placeholder format: ${stats.prefix}<CATEGORY>_<hash12>__`;

const configHint = `To enable via settings.json (requires restart):
"security": {
"redaction": {
"enabled": true,
"placeholderPrefix": "${stats.prefix}",
"keywords": { "example-secret-123": "API_KEY" },
"patterns": { "ghp_[A-Za-z0-9]{36}": "GITHUB_TOKEN" },
"builtins": ["email", "uuid", "ipv4"],
"exclude": ["localhost", "127.0.0.1"],
"ttlMinutes": 60,
"maxSize": 10000
}
}`;

return [
'VibeGuard-style client-side redaction',
enabledLine,
mappingsLine,
formatLine,
'',
'Usage:',
' /vibeguard status',
' /vibeguard on',
' /vibeguard off',
'',
configHint,
].join('\n');
}

export const vibeguardCommand: SlashCommand = {
name: 'vibeguard',
kind: CommandKind.BUILT_IN,
get description() {
return t('Manage client-side redaction (VibeGuard-style placeholders)');
},
action: async (context, args): Promise<MessageActionReturn> => {
const config = context.services.config;
if (!config) {
return {
type: 'message',
messageType: 'error',
content: t('Config is not available.'),
};
}

const sub = args.trim().toLowerCase();
if (sub === 'on' || sub === 'enable') {
config.setRedactionEnabled(true);
} else if (sub === 'off' || sub === 'disable') {
config.setRedactionEnabled(false);
} else if (sub === '' || sub === 'status') {
// no-op
} else {
return {
type: 'message',
messageType: 'error',
content: `Unknown subcommand: ${sub}\n\nTry: /vibeguard status|on|off`,
};
}

const stats = config.getRedactionManager().getStats();
return {
type: 'message',
messageType: 'info',
content: formatStatusMessage(stats),
};
},
};
19 changes: 19 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ import {
setDebugLogSession,
type DebugLogger,
} from '../utils/debugLogger.js';
import {
RedactionManager,
type RedactionConfig,
} from '../security/redaction.js';

import {
ModelsConfig,
Expand Down Expand Up @@ -377,6 +381,11 @@ export interface ConfigParameters {
channel?: string;
/** Model providers configuration grouped by authType */
modelProvidersConfig?: ModelProvidersConfig;
/**
* Client-side redaction configuration (default off).
* This is applied right before any provider request is sent.
*/
redaction?: RedactionConfig;
/** Warnings generated during configuration resolution */
warnings?: string[];
}
Expand Down Expand Up @@ -519,6 +528,7 @@ export class Config {
private readonly eventEmitter?: EventEmitter;
private readonly channel: string | undefined;
private readonly defaultFileEncoding: FileEncodingType;
private readonly redactionManager: RedactionManager;

constructor(params: ConfigParameters) {
this.sessionId = params.sessionId ?? randomUUID();
Expand Down Expand Up @@ -633,6 +643,7 @@ export class Config {
this.enableToolOutputTruncation = params.enableToolOutputTruncation ?? true;
this.channel = params.channel;
this.defaultFileEncoding = params.defaultFileEncoding ?? FileEncoding.UTF8;
this.redactionManager = new RedactionManager(params.redaction);
this.storage = new Storage(this.targetDir);
this.inputFormat = params.inputFormat ?? InputFormat.TEXT;
this.fileExclusions = new FileExclusions(this);
Expand Down Expand Up @@ -1262,6 +1273,14 @@ export class Config {
return this.accessibility;
}

getRedactionManager(): RedactionManager {
return this.redactionManager;
}

setRedactionEnabled(enabled: boolean): void {
this.redactionManager.setEnabled(enabled);
}

getTelemetryEnabled(): boolean {
return this.telemetrySettings.enabled ?? false;
}
Expand Down
14 changes: 9 additions & 5 deletions packages/core/src/core/contentGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import { GoogleGenAI } from '@google/genai';
import type { Config } from '../config/config.js';
import { LoggingContentGenerator } from './loggingContentGenerator/index.js';
import { RedactingContentGenerator } from './redactingContentGenerator/redactingContentGenerator.js';
import { RedactionManager } from '../security/redaction.js';

vi.mock('@google/genai');

Expand All @@ -22,6 +24,7 @@ describe('createContentGenerator', () => {
getUsageStatisticsEnabled: () => true,
getContentGeneratorConfig: () => ({}),
getCliVersion: () => '1.0.0',
getRedactionManager: () => new RedactionManager(undefined),
} as unknown as Config;

const mockGenerator = {
Expand All @@ -46,17 +49,18 @@ describe('createContentGenerator', () => {
},
},
});
// We expect it to be a LoggingContentGenerator wrapping a GeminiContentGenerator
expect(generator).toBeInstanceOf(LoggingContentGenerator);
const wrapped = (generator as LoggingContentGenerator).getWrapped();
expect(wrapped).toBeDefined();
// We expect it to be a RedactingContentGenerator wrapping a LoggingContentGenerator
expect(generator).toBeInstanceOf(RedactingContentGenerator);
const wrapped = (generator as RedactingContentGenerator).getWrapped();
expect(wrapped).toBeInstanceOf(LoggingContentGenerator);
});

it('should create a Gemini content generator with client install id logging disabled', async () => {
const mockConfig = {
getUsageStatisticsEnabled: () => false,
getContentGeneratorConfig: () => ({}),
getCliVersion: () => '1.0.0',
getRedactionManager: () => new RedactionManager(undefined),
} as unknown as Config;
const mockGenerator = {
models: {},
Expand All @@ -79,7 +83,7 @@ describe('createContentGenerator', () => {
},
},
});
expect(generator).toBeInstanceOf(LoggingContentGenerator);
expect(generator).toBeInstanceOf(RedactingContentGenerator);
});
});

Expand Down
Loading