From f71455104e50a499e260beb1cbdcf6398ab2807a Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:34:48 -0500 Subject: [PATCH 1/6] feat(kiloclaw): wire Exa provider in bundled runtime image --- .../controller/src/config-writer.test.ts | 54 ++++++++++++++++ .../kiloclaw/controller/src/config-writer.ts | 58 ++++++++++++++++-- .../kiloclaw-customizer/openclaw.plugin.json | 12 +++- .../src/kilo-exa-web-search-provider.test.ts | 32 ++++++++++ .../src/kilo-exa-web-search-provider.ts | 61 +++++++++++++++++-- 5 files changed, 206 insertions(+), 11 deletions(-) diff --git a/services/kiloclaw/controller/src/config-writer.test.ts b/services/kiloclaw/controller/src/config-writer.test.ts index fe03506b1..b6b1a0d5c 100644 --- a/services/kiloclaw/controller/src/config-writer.test.ts +++ b/services/kiloclaw/controller/src/config-writer.test.ts @@ -153,6 +153,60 @@ describe('generateBaseConfig', () => { expect(config.tools.web.search.provider).toBe('brave'); }); + it('selects kilo-exa provider when KILO_EXA_SEARCH_MODE=kilo-proxy', () => { + const { deps } = fakeDeps(); + const env = { ...minimalEnv(), KILO_EXA_SEARCH_MODE: 'kilo-proxy' }; + const config = generateBaseConfig(env, '/tmp/openclaw.json', deps); + + expect(config.tools.web.search.provider).toBe('kilo-exa'); + expect(config.tools.web.search.enabled).toBe(true); + expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(true); + }); + + it('switches to brave when Exa is disabled and BRAVE_API_KEY is configured', () => { + const existing = JSON.stringify({ + tools: { + web: { + search: { + provider: 'kilo-exa', + }, + }, + }, + }); + const { deps } = fakeDeps(existing); + const env = { + ...minimalEnv(), + KILO_EXA_SEARCH_MODE: 'disabled', + BRAVE_API_KEY: 'BSA' + 'A'.repeat(20), + }; + const config = generateBaseConfig(env, '/tmp/openclaw.json', deps); + + expect(config.tools.web.search.provider).toBe('brave'); + expect(config.tools.web.search.enabled).toBe(true); + expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(false); + }); + + it('clears kilo-exa provider when Exa is disabled and Brave is not configured', () => { + const existing = JSON.stringify({ + tools: { + web: { + search: { + provider: 'kilo-exa', + }, + }, + }, + }); + const { deps } = fakeDeps(existing); + const env = { + ...minimalEnv(), + KILO_EXA_SEARCH_MODE: 'disabled', + }; + const config = generateBaseConfig(env, '/tmp/openclaw.json', deps); + + expect(config.tools.web.search.provider).toBeUndefined(); + expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(false); + }); + it('defaults tool profile to full when not previously set', () => { const { deps } = fakeDeps(); const config = generateBaseConfig(minimalEnv(), '/tmp/openclaw.json', deps); diff --git a/services/kiloclaw/controller/src/config-writer.ts b/services/kiloclaw/controller/src/config-writer.ts index 1d4223707..36f4cc22d 100644 --- a/services/kiloclaw/controller/src/config-writer.ts +++ b/services/kiloclaw/controller/src/config-writer.ts @@ -55,6 +55,24 @@ const ONBOARD_FLAGS = [ '--skip-health', ] as const; +const KILOCLAW_CUSTOMIZER_PLUGIN_ID = 'kiloclaw-customizer'; +const KILOCLAW_CUSTOMIZER_PLUGIN_PATH = '/usr/local/lib/node_modules/@kiloclaw/kiloclaw-customizer'; +const KILO_EXA_PROVIDER_ID = 'kilo-exa'; + +type KiloExaSearchMode = 'kilo-proxy' | 'disabled'; + +function resolveKiloExaSearchMode(value: string | undefined): KiloExaSearchMode { + const normalized = value?.trim().toLowerCase(); + if (normalized === 'kilo-proxy') { + return 'kilo-proxy'; + } + if (normalized === 'disabled' || normalized === undefined || normalized === '') { + return 'disabled'; + } + console.warn(`Unknown KILO_EXA_SEARCH_MODE value "${value}"; treating as "disabled"`); + return 'disabled'; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type ConfigObject = Record; @@ -272,14 +290,42 @@ export function generateBaseConfig( config.plugins.load.paths = Array.isArray(config.plugins.load.paths) ? config.plugins.load.paths : []; - const customizerPluginPath = '/usr/local/lib/node_modules/@kiloclaw/kiloclaw-customizer'; - if (!(config.plugins.load.paths as string[]).includes(customizerPluginPath)) { - (config.plugins.load.paths as string[]).push(customizerPluginPath); + if (!(config.plugins.load.paths as string[]).includes(KILOCLAW_CUSTOMIZER_PLUGIN_PATH)) { + (config.plugins.load.paths as string[]).push(KILOCLAW_CUSTOMIZER_PLUGIN_PATH); } config.plugins.entries = config.plugins.entries ?? {}; - const customizerEntry = 'kiloclaw-customizer'; - config.plugins.entries[customizerEntry] = config.plugins.entries[customizerEntry] ?? {}; - config.plugins.entries[customizerEntry].enabled = true; + config.plugins.entries[KILOCLAW_CUSTOMIZER_PLUGIN_ID] = + config.plugins.entries[KILOCLAW_CUSTOMIZER_PLUGIN_ID] ?? {}; + config.plugins.entries[KILOCLAW_CUSTOMIZER_PLUGIN_ID].enabled = true; + + const customizerPluginConfig = config.plugins.entries[KILOCLAW_CUSTOMIZER_PLUGIN_ID].config ?? {}; + const customizerWebSearchConfig = customizerPluginConfig.webSearch ?? {}; + + const kiloExaSearchMode = resolveKiloExaSearchMode(env.KILO_EXA_SEARCH_MODE); + if (kiloExaSearchMode === 'kilo-proxy') { + customizerWebSearchConfig.enabled = true; + config.tools = config.tools ?? {}; + config.tools.web = config.tools.web ?? {}; + config.tools.web.search = config.tools.web.search ?? {}; + config.tools.web.search.enabled = true; + config.tools.web.search.provider = KILO_EXA_PROVIDER_ID; + } else { + customizerWebSearchConfig.enabled = false; + + const braveConfigured = Boolean(env.BRAVE_API_KEY?.trim()); + if (braveConfigured) { + config.tools = config.tools ?? {}; + config.tools.web = config.tools.web ?? {}; + config.tools.web.search = config.tools.web.search ?? {}; + config.tools.web.search.enabled = true; + config.tools.web.search.provider = 'brave'; + } else if (config.tools?.web?.search?.provider === KILO_EXA_PROVIDER_ID) { + delete config.tools.web.search.provider; + } + } + + customizerPluginConfig.webSearch = customizerWebSearchConfig; + config.plugins.entries[KILOCLAW_CUSTOMIZER_PLUGIN_ID].config = customizerPluginConfig; // Telegram if (env.TELEGRAM_BOT_TOKEN) { diff --git a/services/kiloclaw/plugins/kiloclaw-customizer/openclaw.plugin.json b/services/kiloclaw/plugins/kiloclaw-customizer/openclaw.plugin.json index 9ba06146b..e6da5e539 100644 --- a/services/kiloclaw/plugins/kiloclaw-customizer/openclaw.plugin.json +++ b/services/kiloclaw/plugins/kiloclaw-customizer/openclaw.plugin.json @@ -8,6 +8,16 @@ "configSchema": { "type": "object", "additionalProperties": false, - "properties": {} + "properties": { + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } } } diff --git a/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.test.ts b/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.test.ts index a82a73550..b87de2707 100644 --- a/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.test.ts +++ b/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.test.ts @@ -38,6 +38,38 @@ describe('kilo-exa web search provider', () => { }); }); + it('does not create a tool when plugin webSearch.enabled is false', () => { + const provider = createKiloExaWebSearchProvider(); + const tool = provider.createTool({ + config: { + plugins: { + entries: { + 'kiloclaw-customizer': { + config: { + webSearch: { + enabled: false, + }, + }, + }, + }, + }, + }, + searchConfig: {}, + }); + + expect(tool).toBeNull(); + }); + + it('applySelectionConfig enables plugin web search flag', () => { + const provider = createKiloExaWebSearchProvider(); + const config = provider.applySelectionConfig?.({}) ?? {}; + + expect(config.plugins?.entries?.['kiloclaw-customizer']?.enabled).toBe(true); + expect(config.plugins?.entries?.['kiloclaw-customizer']?.config?.webSearch?.enabled).toBe( + true + ); + }); + it('normalizes uppercase freshness values', async () => { webSearchSdkStub.setPostHandler(async () => ({ results: [] })); const tool = getTool(); diff --git a/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.ts b/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.ts index 784d26f4e..47b468c01 100644 --- a/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.ts +++ b/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.ts @@ -21,6 +21,7 @@ import { } from 'openclaw/plugin-sdk/provider-web-search'; const KILO_EXA_PROVIDER_ID = 'kilo-exa'; +const KILOCLAW_CUSTOMIZER_PLUGIN_ID = 'kiloclaw-customizer'; const DEFAULT_KILO_API_ORIGIN = 'https://api.kilo.ai'; const EXA_SEARCH_TYPES = ['auto', 'neural', 'fast', 'deep', 'deep-reasoning', 'instant'] as const; const EXA_FRESHNESS_VALUES = ['day', 'week', 'month', 'year'] as const; @@ -66,6 +67,49 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } +function resolveCustomizerWebSearchConfig(config: { + plugins?: { + entries?: Record; + }; +} | undefined): Record | undefined { + const pluginEntry = config?.plugins?.entries?.[KILOCLAW_CUSTOMIZER_PLUGIN_ID]; + if (!isRecord(pluginEntry)) { + return undefined; + } + const pluginConfig = isRecord(pluginEntry.config) ? pluginEntry.config : undefined; + return isRecord(pluginConfig?.webSearch) ? pluginConfig.webSearch : undefined; +} + +function setCustomizerWebSearchEnabled( + config: { + plugins?: { + entries?: Record; + }; + }, + enabled: boolean +): void { + const existingEntries = config.plugins?.entries; + const entries = isRecord(existingEntries) ? existingEntries : {}; + const existingEntry = entries[KILOCLAW_CUSTOMIZER_PLUGIN_ID]; + const pluginEntry = isRecord(existingEntry) ? existingEntry : {}; + const existingPluginConfig = pluginEntry.config; + const pluginConfig = isRecord(existingPluginConfig) ? existingPluginConfig : {}; + const existingWebSearch = pluginConfig.webSearch; + const webSearch = isRecord(existingWebSearch) ? existingWebSearch : {}; + + webSearch.enabled = enabled; + pluginConfig.webSearch = webSearch; + pluginEntry.config = pluginConfig; + + config.plugins = { + ...(config.plugins ?? {}), + entries: { + ...entries, + [KILOCLAW_CUSTOMIZER_PLUGIN_ID]: pluginEntry, + }, + }; +} + function parsePositiveInteger(value: unknown): number | undefined { return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined; } @@ -658,11 +702,20 @@ export function createKiloExaWebSearchProvider(): WebSearchProviderPlugin { getScopedCredentialValue(searchConfig, KILO_EXA_PROVIDER_ID), setCredentialValue: (searchConfigTarget, value) => setScopedCredentialValue(searchConfigTarget, KILO_EXA_PROVIDER_ID, value), - applySelectionConfig: config => enablePluginInConfig(config, 'kiloclaw-customizer').config, - createTool: ctx => - createKiloExaToolDefinition( + applySelectionConfig: config => { + const next = enablePluginInConfig(config, KILOCLAW_CUSTOMIZER_PLUGIN_ID).config; + setCustomizerWebSearchEnabled(next, true); + return next; + }, + createTool: ctx => { + const pluginWebSearchConfig = resolveCustomizerWebSearchConfig(ctx.config); + if (pluginWebSearchConfig?.enabled === false) { + return null; + } + return createKiloExaToolDefinition( isSearchConfigRecord(ctx.searchConfig) ? ctx.searchConfig : undefined - ), + ); + }, }; } From 17215d99ba53d9924afad6305af124f075584f49 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:09:24 -0500 Subject: [PATCH 2/6] feat(kiloclaw): auto-assign Exa when search provider is unset --- .../controller/src/config-writer.test.ts | 32 +++++++++++++++++-- .../kiloclaw/controller/src/config-writer.ts | 24 ++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/services/kiloclaw/controller/src/config-writer.test.ts b/services/kiloclaw/controller/src/config-writer.test.ts index b6b1a0d5c..ae8178e6b 100644 --- a/services/kiloclaw/controller/src/config-writer.test.ts +++ b/services/kiloclaw/controller/src/config-writer.test.ts @@ -153,6 +153,33 @@ describe('generateBaseConfig', () => { expect(config.tools.web.search.provider).toBe('brave'); }); + it('auto-assigns kilo-exa when provider is missing and mode is unset', () => { + const existing = JSON.stringify({ tools: { web: { search: {} } } }); + const { deps } = fakeDeps(existing); + const config = generateBaseConfig(minimalEnv(), '/tmp/openclaw.json', deps); + + expect(config.tools.web.search.provider).toBe('kilo-exa'); + expect(config.tools.web.search.enabled).toBe(true); + expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(true); + }); + + it('preserves explicit kilo-exa provider when mode is unset', () => { + const existing = JSON.stringify({ + tools: { + web: { + search: { + provider: 'kilo-exa', + }, + }, + }, + }); + const { deps } = fakeDeps(existing); + const config = generateBaseConfig(minimalEnv(), '/tmp/openclaw.json', deps); + + expect(config.tools.web.search.provider).toBe('kilo-exa'); + expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(true); + }); + it('selects kilo-exa provider when KILO_EXA_SEARCH_MODE=kilo-proxy', () => { const { deps } = fakeDeps(); const env = { ...minimalEnv(), KILO_EXA_SEARCH_MODE: 'kilo-proxy' }; @@ -1038,7 +1065,7 @@ describe('writeBaseConfig', () => { expect(config.tools.profile).toBe('full'); }); - it('does not steer web search provider on config restore path', () => { + it('auto-assigns Exa web search provider on restore path when provider is missing', () => { const { deps, written } = fakeDeps(); const env = minimalEnv(); delete env.KILOCLAW_FRESH_INSTALL; @@ -1046,7 +1073,8 @@ describe('writeBaseConfig', () => { writeBaseConfig(env, '/tmp/openclaw.json', deps); const config = JSON.parse(written[0].data); - expect(config.tools?.web?.search?.provider).toBeUndefined(); + expect(config.tools?.web?.search?.provider).toBe('kilo-exa'); + expect(config.tools?.web?.search?.enabled).toBe(true); }); it('throws if KILOCODE_API_KEY is missing', () => { diff --git a/services/kiloclaw/controller/src/config-writer.ts b/services/kiloclaw/controller/src/config-writer.ts index 36f4cc22d..4588ee4d4 100644 --- a/services/kiloclaw/controller/src/config-writer.ts +++ b/services/kiloclaw/controller/src/config-writer.ts @@ -61,14 +61,19 @@ const KILO_EXA_PROVIDER_ID = 'kilo-exa'; type KiloExaSearchMode = 'kilo-proxy' | 'disabled'; -function resolveKiloExaSearchMode(value: string | undefined): KiloExaSearchMode { +type KiloExaSearchModeState = KiloExaSearchMode | 'unset'; + +function resolveKiloExaSearchMode(value: string | undefined): KiloExaSearchModeState { const normalized = value?.trim().toLowerCase(); if (normalized === 'kilo-proxy') { return 'kilo-proxy'; } - if (normalized === 'disabled' || normalized === undefined || normalized === '') { + if (normalized === 'disabled') { return 'disabled'; } + if (normalized === undefined || normalized === '') { + return 'unset'; + } console.warn(`Unknown KILO_EXA_SEARCH_MODE value "${value}"; treating as "disabled"`); return 'disabled'; } @@ -300,6 +305,9 @@ export function generateBaseConfig( const customizerPluginConfig = config.plugins.entries[KILOCLAW_CUSTOMIZER_PLUGIN_ID].config ?? {}; const customizerWebSearchConfig = customizerPluginConfig.webSearch ?? {}; + const searchProvider = config.tools?.web?.search?.provider; + const hasExplicitSearchProvider = + typeof searchProvider === 'string' && searchProvider.trim().length > 0; const kiloExaSearchMode = resolveKiloExaSearchMode(env.KILO_EXA_SEARCH_MODE); if (kiloExaSearchMode === 'kilo-proxy') { @@ -309,7 +317,7 @@ export function generateBaseConfig( config.tools.web.search = config.tools.web.search ?? {}; config.tools.web.search.enabled = true; config.tools.web.search.provider = KILO_EXA_PROVIDER_ID; - } else { + } else if (kiloExaSearchMode === 'disabled') { customizerWebSearchConfig.enabled = false; const braveConfigured = Boolean(env.BRAVE_API_KEY?.trim()); @@ -322,6 +330,16 @@ export function generateBaseConfig( } else if (config.tools?.web?.search?.provider === KILO_EXA_PROVIDER_ID) { delete config.tools.web.search.provider; } + } else if (hasExplicitSearchProvider) { + customizerWebSearchConfig.enabled = config.tools?.web?.search?.provider === KILO_EXA_PROVIDER_ID; + } else { + customizerWebSearchConfig.enabled = true; + config.tools = config.tools ?? {}; + config.tools.web = config.tools.web ?? {}; + config.tools.web.search = config.tools.web.search ?? {}; + config.tools.web.search.enabled = true; + config.tools.web.search.provider = KILO_EXA_PROVIDER_ID; + console.log('[config-writer] Auto-assigned web search provider to kilo-exa (mode=unset)'); } customizerPluginConfig.webSearch = customizerWebSearchConfig; From 174b1cd3cb1dfb079c3e6bb7535374dce9a4e48e Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:17:58 -0500 Subject: [PATCH 3/6] style(kiloclaw): format Exa runtime controller and plugin files --- services/kiloclaw/controller/src/config-writer.ts | 3 ++- .../src/kilo-exa-web-search-provider.test.ts | 4 +--- .../src/kilo-exa-web-search-provider.ts | 14 +++++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/services/kiloclaw/controller/src/config-writer.ts b/services/kiloclaw/controller/src/config-writer.ts index 4588ee4d4..fc472f47c 100644 --- a/services/kiloclaw/controller/src/config-writer.ts +++ b/services/kiloclaw/controller/src/config-writer.ts @@ -331,7 +331,8 @@ export function generateBaseConfig( delete config.tools.web.search.provider; } } else if (hasExplicitSearchProvider) { - customizerWebSearchConfig.enabled = config.tools?.web?.search?.provider === KILO_EXA_PROVIDER_ID; + customizerWebSearchConfig.enabled = + config.tools?.web?.search?.provider === KILO_EXA_PROVIDER_ID; } else { customizerWebSearchConfig.enabled = true; config.tools = config.tools ?? {}; diff --git a/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.test.ts b/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.test.ts index b87de2707..4eafdc60a 100644 --- a/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.test.ts +++ b/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.test.ts @@ -65,9 +65,7 @@ describe('kilo-exa web search provider', () => { const config = provider.applySelectionConfig?.({}) ?? {}; expect(config.plugins?.entries?.['kiloclaw-customizer']?.enabled).toBe(true); - expect(config.plugins?.entries?.['kiloclaw-customizer']?.config?.webSearch?.enabled).toBe( - true - ); + expect(config.plugins?.entries?.['kiloclaw-customizer']?.config?.webSearch?.enabled).toBe(true); }); it('normalizes uppercase freshness values', async () => { diff --git a/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.ts b/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.ts index 47b468c01..396ea4e0d 100644 --- a/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.ts +++ b/services/kiloclaw/plugins/kiloclaw-customizer/src/kilo-exa-web-search-provider.ts @@ -67,11 +67,15 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } -function resolveCustomizerWebSearchConfig(config: { - plugins?: { - entries?: Record; - }; -} | undefined): Record | undefined { +function resolveCustomizerWebSearchConfig( + config: + | { + plugins?: { + entries?: Record; + }; + } + | undefined +): Record | undefined { const pluginEntry = config?.plugins?.entries?.[KILOCLAW_CUSTOMIZER_PLUGIN_ID]; if (!isRecord(pluginEntry)) { return undefined; From fd8d2d0c604c30a480026039031c51a2c3a42e56 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:01:31 -0500 Subject: [PATCH 4/6] fix(kiloclaw): preserve explicit search providers when Exa disabled --- .../controller/src/config-writer.test.ts | 22 +++++++++++++++++++ .../kiloclaw/controller/src/config-writer.ts | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/services/kiloclaw/controller/src/config-writer.test.ts b/services/kiloclaw/controller/src/config-writer.test.ts index ae8178e6b..640d9a419 100644 --- a/services/kiloclaw/controller/src/config-writer.test.ts +++ b/services/kiloclaw/controller/src/config-writer.test.ts @@ -213,6 +213,28 @@ describe('generateBaseConfig', () => { expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(false); }); + it('preserves explicit non-Exa provider when Exa is disabled and BRAVE_API_KEY is configured', () => { + const existing = JSON.stringify({ + tools: { + web: { + search: { + provider: 'perplexity', + }, + }, + }, + }); + const { deps } = fakeDeps(existing); + const env = { + ...minimalEnv(), + KILO_EXA_SEARCH_MODE: 'disabled', + BRAVE_API_KEY: 'BSA' + 'A'.repeat(20), + }; + const config = generateBaseConfig(env, '/tmp/openclaw.json', deps); + + expect(config.tools.web.search.provider).toBe('perplexity'); + expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(false); + }); + it('clears kilo-exa provider when Exa is disabled and Brave is not configured', () => { const existing = JSON.stringify({ tools: { diff --git a/services/kiloclaw/controller/src/config-writer.ts b/services/kiloclaw/controller/src/config-writer.ts index fc472f47c..a14e1c9ae 100644 --- a/services/kiloclaw/controller/src/config-writer.ts +++ b/services/kiloclaw/controller/src/config-writer.ts @@ -321,7 +321,10 @@ export function generateBaseConfig( customizerWebSearchConfig.enabled = false; const braveConfigured = Boolean(env.BRAVE_API_KEY?.trim()); - if (braveConfigured) { + if ( + braveConfigured && + (!hasExplicitSearchProvider || config.tools?.web?.search?.provider === KILO_EXA_PROVIDER_ID) + ) { config.tools = config.tools ?? {}; config.tools.web = config.tools.web ?? {}; config.tools.web.search = config.tools.web.search ?? {}; From 1fda0453998ef583db97c22861b40797d6faaae4 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:17:17 -0500 Subject: [PATCH 5/6] refactor(kiloclaw): unify Exa force and auto-assign branches --- services/kiloclaw/controller/src/config-writer.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/services/kiloclaw/controller/src/config-writer.ts b/services/kiloclaw/controller/src/config-writer.ts index a14e1c9ae..d6a57022b 100644 --- a/services/kiloclaw/controller/src/config-writer.ts +++ b/services/kiloclaw/controller/src/config-writer.ts @@ -310,13 +310,18 @@ export function generateBaseConfig( typeof searchProvider === 'string' && searchProvider.trim().length > 0; const kiloExaSearchMode = resolveKiloExaSearchMode(env.KILO_EXA_SEARCH_MODE); - if (kiloExaSearchMode === 'kilo-proxy') { + const shouldForceExa = kiloExaSearchMode === 'kilo-proxy'; + const shouldAutoAssignExa = kiloExaSearchMode === 'unset' && !hasExplicitSearchProvider; + if (shouldForceExa || shouldAutoAssignExa) { customizerWebSearchConfig.enabled = true; config.tools = config.tools ?? {}; config.tools.web = config.tools.web ?? {}; config.tools.web.search = config.tools.web.search ?? {}; config.tools.web.search.enabled = true; config.tools.web.search.provider = KILO_EXA_PROVIDER_ID; + if (shouldAutoAssignExa) { + console.log('[config-writer] Auto-assigned web search provider to kilo-exa (mode=unset)'); + } } else if (kiloExaSearchMode === 'disabled') { customizerWebSearchConfig.enabled = false; @@ -336,14 +341,6 @@ export function generateBaseConfig( } else if (hasExplicitSearchProvider) { customizerWebSearchConfig.enabled = config.tools?.web?.search?.provider === KILO_EXA_PROVIDER_ID; - } else { - customizerWebSearchConfig.enabled = true; - config.tools = config.tools ?? {}; - config.tools.web = config.tools.web ?? {}; - config.tools.web.search = config.tools.web.search ?? {}; - config.tools.web.search.enabled = true; - config.tools.web.search.provider = KILO_EXA_PROVIDER_ID; - console.log('[config-writer] Auto-assigned web search provider to kilo-exa (mode=unset)'); } customizerPluginConfig.webSearch = customizerWebSearchConfig; From 03a23b6f7f249c692f4849b07ce7b961c4794bf7 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:34:49 -0500 Subject: [PATCH 6/6] test(kiloclaw): lock Exa auto-assign behavior for disabled search --- .../controller/src/config-writer.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/services/kiloclaw/controller/src/config-writer.test.ts b/services/kiloclaw/controller/src/config-writer.test.ts index 640d9a419..824c8afa9 100644 --- a/services/kiloclaw/controller/src/config-writer.test.ts +++ b/services/kiloclaw/controller/src/config-writer.test.ts @@ -163,6 +163,24 @@ describe('generateBaseConfig', () => { expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(true); }); + it('auto-assigns kilo-exa even when web search was explicitly disabled but provider is missing', () => { + const existing = JSON.stringify({ + tools: { + web: { + search: { + enabled: false, + }, + }, + }, + }); + const { deps } = fakeDeps(existing); + const config = generateBaseConfig(minimalEnv(), '/tmp/openclaw.json', deps); + + expect(config.tools.web.search.provider).toBe('kilo-exa'); + expect(config.tools.web.search.enabled).toBe(true); + expect(config.plugins.entries['kiloclaw-customizer'].config.webSearch.enabled).toBe(true); + }); + it('preserves explicit kilo-exa provider when mode is unset', () => { const existing = JSON.stringify({ tools: {