diff --git a/packages/cli/src/tests/exec-runner.test.ts b/packages/cli/src/tests/exec-runner.test.ts index 1faa40ea..62e92a12 100644 --- a/packages/cli/src/tests/exec-runner.test.ts +++ b/packages/cli/src/tests/exec-runner.test.ts @@ -33,6 +33,7 @@ function createSettings( telemetryEnabled: false, permissions, enabledSkills: {}, + enabledSkillsDefaultOff: false, statusline: { enabled: false, refreshMs: 1000, separator: " | ", providers: [] }, }; } diff --git a/packages/core/src/prompt.ts b/packages/core/src/prompt.ts index bb1a4b02..5b5cf712 100644 --- a/packages/core/src/prompt.ts +++ b/packages/core/src/prompt.ts @@ -103,6 +103,7 @@ export type PromptToolOptions = { type DefaultSkillPromptOptions = { enabledSkills?: Record; + enabledSkillsDefaultOff?: boolean; }; const DEFAULT_SKILL_TEMPLATES = ["karpathy-guidelines.md"]; @@ -161,13 +162,18 @@ function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): s function readDefaultSkillDocs( extensionRoot: string, - enabledSkills: Record = {} + enabledSkills: Record = {}, + enabledSkillsDefaultOff = false ): Array<{ name: string; content: string }> { const skillsDir = path.join(extensionRoot, "templates", "skills"); return DEFAULT_SKILL_TEMPLATES.map((entry) => { const fullPath = path.join(skillsDir, entry); const name = path.basename(entry, ".md"); - if (enabledSkills[name] === false) { + // Whitelist mode (#178): when enabledSkillsDefaultOff is set, only skills + // explicitly enabled with `true` are injected; otherwise exclude only + // skills explicitly disabled with `false`. + const excluded = enabledSkillsDefaultOff ? enabledSkills[name] !== true : enabledSkills[name] === false; + if (excluded) { return null; } try { @@ -182,7 +188,7 @@ function readDefaultSkillDocs( } export function getDefaultSkillPrompt(options: DefaultSkillPromptOptions = {}): string { - const skillDocs = readDefaultSkillDocs(getExtensionRoot(), options.enabledSkills); + const skillDocs = readDefaultSkillDocs(getExtensionRoot(), options.enabledSkills, options.enabledSkillsDefaultOff); if (skillDocs.length === 0) { return ""; } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index b9252eaa..78e69e31 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -370,6 +370,7 @@ export class SessionManager { mcpServers?: Record; permissions?: Required; enabledSkills?: Record; + enabledSkillsDefaultOff?: boolean; }; private readonly onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; private readonly onSessionEntryUpdated?: (entry: SessionEntry) => void; @@ -1181,7 +1182,10 @@ ${agentInstructions} const systemMessage = this.buildSystemMessage(sessionId, systemPrompt); this.appendSessionMessage(sessionId, systemMessage); - const defaultSkillPrompt = getDefaultSkillPrompt({ enabledSkills: this.getResolvedSettings().enabledSkills }); + const defaultSkillPrompt = getDefaultSkillPrompt({ + enabledSkills: this.getResolvedSettings().enabledSkills, + enabledSkillsDefaultOff: this.getResolvedSettings().enabledSkillsDefaultOff, + }); if (defaultSkillPrompt) { const defaultSkillMessage = this.buildSystemMessage(sessionId, defaultSkillPrompt); this.appendSessionMessage(sessionId, defaultSkillMessage); diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index da0e2d94..d486f468 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -95,6 +95,7 @@ export type DeepcodingSettings = { mcpServers?: Record; permissions?: PermissionSettings; enabledSkills?: EnabledSkillsSettings; + enabledSkillsDefaultOff?: boolean; statusline?: StatusLineSettings; }; @@ -115,6 +116,7 @@ export type ResolvedDeepcodingSettings = { mcpServers?: Record; permissions: Required; enabledSkills: EnabledSkillsSettings; + enabledSkillsDefaultOff: boolean; statusline: ResolvedStatusLineSettings; }; @@ -585,6 +587,12 @@ export function resolveSettingsSources( trimString(userSettings?.webSearchTool) || ""; + const enabledSkillsDefaultOff = + parseBoolean(systemEnv.ENABLED_SKILLS_DEFAULT_OFF) ?? + parseBoolean(projectSettings?.enabledSkillsDefaultOff) ?? + parseBoolean(userSettings?.enabledSkillsDefaultOff) ?? + false; + return { env, apiKey: trimString(env.API_KEY) || undefined, @@ -602,6 +610,7 @@ export function resolveSettingsSources( mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv), permissions: mergePermissions(userSettings, projectSettings), enabledSkills: mergeEnabledSkills(userSettings, projectSettings), + enabledSkillsDefaultOff, statusline: mergeStatusLine(userSettings, projectSettings), }; } diff --git a/packages/core/src/tests/prompt.test.ts b/packages/core/src/tests/prompt.test.ts index 4fda557a..ea93ce40 100644 --- a/packages/core/src/tests/prompt.test.ts +++ b/packages/core/src/tests/prompt.test.ts @@ -77,6 +77,24 @@ test("getTools includes UpdatePlan with string plan schema", () => { assert.equal((tool.function.parameters.properties.plan as { type?: unknown }).type, "string"); }); +test("getDefaultSkillPrompt honors whitelist mode via enabledSkillsDefaultOff (#178)", () => { + const name = "karpathy-guidelines"; + const defaultPrompt = getDefaultSkillPrompt({}); + const defaultOffPrompt = getDefaultSkillPrompt({ enabledSkillsDefaultOff: true }); + const explicitOnPrompt = getDefaultSkillPrompt({ + enabledSkillsDefaultOff: true, + enabledSkills: { [name]: true }, + }); + const explicitOffPrompt = getDefaultSkillPrompt({ enabledSkills: { [name]: false } }); + + assert.equal(defaultOffPrompt.length, 0, "whitelist mode with no enabled skills injects nothing"); + if (defaultPrompt) { + assert.equal(defaultPrompt.includes(name), true, "default mode injects karpathy-guidelines"); + assert.equal(explicitOnPrompt.includes(name), true, "whitelist mode injects explicitly enabled skill"); + assert.equal(explicitOffPrompt.length, 0, "explicit false excludes the skill in default mode"); + } +}); + test("getTools requires bash sideEffects permission scopes", () => { const tool = getTools().find((candidate) => candidate.function.name === "bash"); assert.ok(tool); diff --git a/packages/core/src/tests/settings-and-notify.test.ts b/packages/core/src/tests/settings-and-notify.test.ts index 93e8dc29..be8fe98f 100644 --- a/packages/core/src/tests/settings-and-notify.test.ts +++ b/packages/core/src/tests/settings-and-notify.test.ts @@ -11,6 +11,22 @@ import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } fr const TEST_PROCESS_ENV = {}; +test("resolveSettings reads enabledSkillsDefaultOff from settings and env (#178)", () => { + const resolved = resolveSettings( + { enabledSkillsDefaultOff: true, env: { API_KEY: "sk-test" } }, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.equal(resolved.enabledSkillsDefaultOff, true); + + const fromEnv = resolveSettings( + { env: { API_KEY: "sk-test" } }, + { model: "default-model", baseURL: "https://default.example.com" }, + { DEEPCODE_ENABLED_SKILLS_DEFAULT_OFF: "true" } + ); + assert.equal(fromEnv.enabledSkillsDefaultOff, true); +}); + test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool", () => { const resolved = resolveSettings( {