Skip to content

Commit ff0ba33

Browse files
authored
feat(brave) add brave search API key catalog entry (#1416)
## Summary - Brave Search API removed their free tier — OpenClaw's web_search tool now requires an API key - Adds a brave-search entry to the Secret Catalog so users can configure their Brave Search API key through the Settings UI - The key is delivered as BRAVE_API_KEY env var to the container, which OpenClaw reads natively — no config-writer or image changes needed Files changed (8): - kiloclaw/packages/secret-catalog/src/catalog.ts — new brave-search catalog entry - kiloclaw/packages/secret-catalog/src/types.ts — add 'brave' to icon key schema - src/app/(app)/claw/components/icons/BraveSearchIcon.tsx — Brave lion logo SVG (from Simple Icons) - src/app/(app)/claw/components/secret-ui-adapter.ts — icon + description mapping - src/app/(app)/claw/components/SettingsTab.tsx — "Search" section above Developer Tools - src/app/(app)/claw/components/changelog-data.ts — changelog entry - kiloclaw/packages/secret-catalog/src/__tests__/catalog.test.ts — catalog test updates - kiloclaw/src/routes/kiloclaw.test.ts — buildConfiguredSecrets test updates ## Verification - pnpm --filter @kilocode/kiloclaw-secret-catalog test — 56/56 passed - pnpm --filter kiloclaw test — 956/956 passed - pnpm run format — clean - pnpm run typecheck — clean - Manual e2e test: Added a real Brave API key via the Settings UI, redeployed instance, confirmed web_search tool works with Brave Search ## Visual Changes - New Search section appears in Settings tab above Developer Tools - Shows Brave lion icon, "Brave Search" label, and single API Key field - Validation: keys must start with BSA followed by 20+ alphanumeric/underscore/hyphen characters - Changelog entry added for the feature <img width="969" height="535" alt="Screenshot 2026-03-23 at 10 17 39 AM" src="https://github.com/user-attachments/assets/4c8705c6-9a97-4376-8c5f-662d5fc46656" /> ## Reviewer Notes - Validation pattern ^BSA[A-Za-z0-9_-]{20,}$ was verified against a real Brave API key — third-party secret scanners (GitGuardian) suggested BSAI prefix but real keys show the 4th char varies - Env var is BRAVE_API_KEY (not BRAVE_SEARCH_API_KEY) — this is what OpenClaw's built-in web_search tool reads - No image rebuild required — the bootstrap generically decrypts all KILOCLAW_ENC_* vars - The some + filter pattern in SettingsTab matches existing sections (github, agentcard, onepassword) — kept consistent rather than introducing a new pattern - Brave lion logo SVG sourced from Simple Icons project, consistent with other branded icons (Discord, Slack, GitHub) in the same directory
2 parents a4433bb + 0edd643 commit ff0ba33

8 files changed

Lines changed: 89 additions & 3 deletions

File tree

kiloclaw/packages/secret-catalog/src/__tests__/catalog.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe('Secret Catalog', () => {
4545
'github',
4646
'credit-card',
4747
'lock',
48+
'brave',
4849
]);
4950
for (const entry of SECRET_CATALOG) {
5051
expect(validIcons.has(entry.icon)).toBe(true);
@@ -119,6 +120,7 @@ describe('Secret Catalog', () => {
119120
'GITHUB_TOKEN',
120121
'GITHUB_USERNAME',
121122
'GITHUB_EMAIL',
123+
'BRAVE_API_KEY',
122124
]);
123125

124126
const catalogEnvVars = new Set(FIELD_KEY_TO_ENV_VAR.values());
@@ -136,6 +138,7 @@ describe('Secret Catalog', () => {
136138
expect(FIELD_KEY_TO_ENV_VAR.get('githubToken')).toBe('GITHUB_TOKEN');
137139
expect(FIELD_KEY_TO_ENV_VAR.get('githubUsername')).toBe('GITHUB_USERNAME');
138140
expect(FIELD_KEY_TO_ENV_VAR.get('githubEmail')).toBe('GITHUB_EMAIL');
141+
expect(FIELD_KEY_TO_ENV_VAR.get('braveSearchApiKey')).toBe('BRAVE_API_KEY');
139142
});
140143

141144
it('ENV_VAR_TO_FIELD_KEY is the exact reverse of FIELD_KEY_TO_ENV_VAR', () => {
@@ -153,6 +156,7 @@ describe('Secret Catalog', () => {
153156
expect(ENV_VAR_TO_FIELD_KEY.get('GITHUB_TOKEN')).toBe('githubToken');
154157
expect(ENV_VAR_TO_FIELD_KEY.get('GITHUB_USERNAME')).toBe('githubUsername');
155158
expect(ENV_VAR_TO_FIELD_KEY.get('GITHUB_EMAIL')).toBe('githubEmail');
159+
expect(ENV_VAR_TO_FIELD_KEY.get('BRAVE_API_KEY')).toBe('braveSearchApiKey');
156160
});
157161
});
158162

@@ -194,10 +198,11 @@ describe('Secret Catalog', () => {
194198

195199
it('returns all tool entries sorted by order', () => {
196200
const tools = getEntriesByCategory('tool');
197-
expect(tools.length).toBe(3);
201+
expect(tools.length).toBe(4);
198202
expect(tools[0].id).toBe('github');
199203
expect(tools[1].id).toBe('agentcard');
200204
expect(tools[2].id).toBe('onepassword');
205+
expect(tools[3].id).toBe('brave-search');
201206
});
202207

203208
it('returns empty array for categories with no entries', () => {
@@ -223,7 +228,8 @@ describe('Secret Catalog', () => {
223228
expect(keys).toContain('githubEmail');
224229
expect(keys).toContain('agentcardApiKey');
225230
expect(keys).toContain('onepasswordServiceAccountToken');
226-
expect(keys.size).toBe(5);
231+
expect(keys).toContain('braveSearchApiKey');
232+
expect(keys.size).toBe(6);
227233
});
228234

229235
it('returns empty set for categories with no entries', () => {
@@ -360,6 +366,21 @@ describe('Secret Catalog', () => {
360366
expect(validateFieldValue('invalid', pattern)).toBe(false);
361367
});
362368

369+
it('accepts valid Brave Search API keys', () => {
370+
const pattern = '^BSA[A-Za-z0-9_-]{20,}$';
371+
// Real key format: BSA + mixed alphanumeric, ~30 chars total
372+
expect(validateFieldValue('BSAq2h7cYupyy704DHyXPFlUx8SinqK', pattern)).toBe(true);
373+
expect(validateFieldValue('BSA' + 'A'.repeat(20), pattern)).toBe(true);
374+
expect(validateFieldValue('BSAIabcDEF_123-456abcDEF1234', pattern)).toBe(true);
375+
});
376+
377+
it('rejects invalid Brave Search API keys', () => {
378+
const pattern = '^BSA[A-Za-z0-9_-]{20,}$';
379+
expect(validateFieldValue('invalid', pattern)).toBe(false);
380+
expect(validateFieldValue('BSAshort', pattern)).toBe(false);
381+
expect(validateFieldValue('bsa' + 'A'.repeat(20), pattern)).toBe(false);
382+
});
383+
363384
it('rejects empty strings', () => {
364385
const pattern = '^\\d{8,}:[A-Za-z0-9_-]{30,50}$';
365386
expect(validateFieldValue('', pattern)).toBe(false);

kiloclaw/packages/secret-catalog/src/catalog.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,27 @@ const SECRET_CATALOG_RAW = [
176176
helpText: 'Create a service account at 1password.com with access to a dedicated vault.',
177177
helpUrl: 'https://developer.1password.com/docs/service-accounts/get-started/',
178178
},
179+
{
180+
id: 'brave-search',
181+
label: 'Brave Search',
182+
category: 'tool',
183+
icon: 'brave',
184+
order: 4,
185+
fields: [
186+
{
187+
key: 'braveSearchApiKey',
188+
label: 'API Key',
189+
placeholder: 'BSA...',
190+
placeholderConfigured: 'Enter new key to replace',
191+
envVar: 'BRAVE_API_KEY',
192+
validationPattern: '^BSA[A-Za-z0-9_-]{20,}$',
193+
validationMessage: 'Brave Search keys start with BSA followed by 20 or more characters.',
194+
maxLength: 200,
195+
},
196+
],
197+
helpText: 'Get an API key from the Brave Search dashboard.',
198+
helpUrl: 'https://brave.com/search/api/',
199+
},
179200
] as const satisfies readonly SecretCatalogEntry[];
180201

181202
// Runtime validation — fails fast at module load if catalog data is malformed

kiloclaw/packages/secret-catalog/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const SecretIconKeySchema = z.enum([
1212
'github',
1313
'credit-card',
1414
'lock',
15+
'brave',
1516
]);
1617

1718
/**

kiloclaw/src/routes/kiloclaw.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('buildConfiguredSecrets', () => {
1818
github: false,
1919
agentcard: false,
2020
onepassword: false,
21+
'brave-search': false,
2122
});
2223
});
2324

@@ -112,7 +113,8 @@ describe('buildConfiguredSecrets', () => {
112113
expect(keys).toContain('discord');
113114
expect(keys).toContain('slack');
114115
expect(keys).toContain('onepassword');
115-
expect(keys).toHaveLength(6);
116+
expect(keys).toContain('brave-search');
117+
expect(keys).toHaveLength(7);
116118
});
117119

118120
it('treats null values as not configured', () => {

src/app/(app)/claw/components/SettingsTab.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,27 @@ export function SettingsTab({
731731
</div>
732732
</div>
733733

734+
{/* ── Search ── */}
735+
{toolEntries.some(e => e.id === 'brave-search') && (
736+
<div>
737+
<h2 className="text-foreground mb-3 text-base font-semibold">Search</h2>
738+
<div className="space-y-3">
739+
{toolEntries
740+
.filter(e => e.id === 'brave-search')
741+
.map(entry => (
742+
<SecretEntrySection
743+
key={entry.id}
744+
entry={entry}
745+
configured={configuredSecrets[entry.id] ?? false}
746+
mutations={mutations}
747+
onSecretsChanged={onSecretsChanged}
748+
isDirty={dirtySecrets.has(entry.id)}
749+
/>
750+
))}
751+
</div>
752+
</div>
753+
)}
754+
734755
{/* ── Developer Tools ── */}
735756
{toolEntries.some(e => e.id === 'github') && (
736757
<div>

src/app/(app)/claw/components/changelog-data.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ export type ChangelogEntry = {
1010

1111
// Newest entries first. Developers add new entries to the top of this array.
1212
export const CHANGELOG_ENTRIES: ChangelogEntry[] = [
13+
{
14+
date: '2026-03-23',
15+
description:
16+
'Added Brave Search integration. Connect your Brave Search API key in Settings to enable the web_search tool. Brave Search removed their free tier, so an API key is now required.',
17+
category: 'feature',
18+
deployHint: 'redeploy_suggested',
19+
},
1320
{
1421
date: '2026-03-19',
1522
description:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function BraveSearchIcon({ className }: { className?: string }) {
2+
return (
3+
<svg viewBox="0 0 24 24" className={className} xmlns="http://www.w3.org/2000/svg">
4+
<path
5+
d="M15.68 0l2.096 2.38s1.84-.512 2.709.358c.868.87 1.584 1.638 1.584 1.638l-.562 1.381.715 2.047s-2.104 7.98-2.35 8.955c-.486 1.919-.818 2.66-2.198 3.633-1.38.972-3.884 2.66-4.293 2.916-.409.256-.92.692-1.38.692-.46 0-.97-.436-1.38-.692a185.796 185.796 0 01-4.293-2.916c-1.38-.973-1.712-1.714-2.197-3.633-.247-.975-2.351-8.955-2.351-8.955l.715-2.047-.562-1.381s.716-.768 1.585-1.638c.868-.87 2.708-.358 2.708-.358L8.321 0h7.36zm-3.679 14.936c-.14 0-1.038.317-1.758.69-.72.373-1.242.637-1.409.742-.167.104-.065.301.087.409.152.107 2.194 1.69 2.393 1.866.198.175.489.464.687.464.198 0 .49-.29.688-.464.198-.175 2.24-1.759 2.392-1.866.152-.108.254-.305.087-.41-.167-.104-.689-.368-1.41-.741-.72-.373-1.617-.69-1.757-.69zm0-11.278s-.409.001-1.022.206-1.278.46-1.584.46c-.307 0-2.581-.434-2.581-.434S4.119 7.152 4.119 7.849c0 .697.339.881.68 1.243l2.02 2.149c.192.203.59.511.356 1.066-.235.555-.58 1.26-.196 1.977.384.716 1.042 1.194 1.464 1.115.421-.08 1.412-.598 1.776-.834.364-.237 1.518-1.19 1.518-1.554 0-.365-1.193-1.02-1.413-1.168-.22-.15-1.226-.725-1.247-.95-.02-.227-.012-.293.284-.851.297-.559.831-1.304.742-1.8-.089-.495-.95-.753-1.565-.986-.615-.232-1.799-.671-1.947-.74-.148-.068-.11-.133.339-.175.448-.043 1.719-.212 2.292-.052.573.16 1.552.403 1.632.532.079.13.149.134.067.579-.081.445-.5 2.581-.541 2.96-.04.38-.12.63.288.724.409.094 1.097.256 1.333.256s.924-.162 1.333-.256c.408-.093.329-.344.288-.723-.04-.38-.46-2.516-.541-2.961-.082-.445-.012-.45.067-.579.08-.129 1.059-.372 1.632-.532.573-.16 1.845.009 2.292.052.449.042.487.107.339.175-.148.069-1.332.508-1.947.74-.615.233-1.476.49-1.565.986-.09.496.445 1.241.742 1.8.297.558.304.624.284.85-.02.226-1.026.802-1.247.95-.22.15-1.413.804-1.413 1.169 0 .364 1.154 1.317 1.518 1.554.364.236 1.355.755 1.776.834.422.079 1.08-.4 1.464-1.115.384-.716.039-1.422-.195-1.977-.235-.555.163-.863.355-1.066l2.02-2.149c.341-.362.68-.546.68-1.243 0-.697-2.695-3.96-2.695-3.96s-2.274.436-2.58.436c-.307 0-.972-.256-1.585-.461-.613-.205-1.022-.206-1.022-.206z"
6+
fill="#FB542B"
7+
/>
8+
</svg>
9+
);
10+
}

src/app/(app)/claw/components/secret-ui-adapter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DiscordIcon } from './icons/DiscordIcon';
66
import { SlackIcon } from './icons/SlackIcon';
77
import { GitHubIcon } from './icons/GitHubIcon';
88
import { AgentCardIcon } from './icons/AgentCardIcon';
9+
import { BraveSearchIcon } from './icons/BraveSearchIcon';
910

1011
const ICON_MAP: Record<SecretIconKey, React.ComponentType<{ className?: string }>> = {
1112
send: TelegramIcon,
@@ -15,6 +16,7 @@ const ICON_MAP: Record<SecretIconKey, React.ComponentType<{ className?: string }
1516
github: GitHubIcon,
1617
'credit-card': AgentCardIcon,
1718
lock: Lock,
19+
brave: BraveSearchIcon,
1820
};
1921

2022
export function getIcon(iconKey: SecretIconKey): React.ComponentType<{ className?: string }> {
@@ -29,6 +31,7 @@ const DESCRIPTION_MAP: Record<string, string> = {
2931
github: 'Connect a GitHub account for code operations',
3032
agentcard: 'Give your bot virtual debit cards for spending',
3133
onepassword: 'Look up credentials and manage vault items via the op CLI',
34+
'brave-search': 'Add a Brave Search API key for web search',
3235
};
3336

3437
export function getDescription(entryId: string): string {

0 commit comments

Comments
 (0)