@openclaw/brave-plugin 2026.9.1-beta.1 → 2026.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { buildBraveWebSearchProviderBase } from "./web-search-shared.js";
2
- import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
2
+ import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-flags";
3
3
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
4
4
  import { mergeScopedSearchConfig, resolveProviderWebSearchPluginConfig } from "openclaw/plugin-sdk/provider-web-search-config-contract";
5
5
  import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -8,7 +8,7 @@ import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
8
8
  * Brave web-search provider factory. It builds the agent tool definition and
9
9
  * lazy-loads HTTP execution only when a search is run.
10
10
  */
11
- const loadBraveWebSearchRuntime = createLazyRuntimeModule(() => import("./brave-web-search-provider.runtime-D1eCbRZi.js"));
11
+ const loadBraveWebSearchRuntime = createLazyRuntimeModule(() => import("./brave-web-search-provider.runtime-CKcjuVbf.js"));
12
12
  const BraveSearchSchema = {
13
13
  type: "object",
14
14
  properties: {
@@ -1,6 +1,7 @@
1
1
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
2
+ import { buildTimeoutAbortSignal } from "openclaw/plugin-sdk/extension-shared";
2
3
  import { assertOkOrThrowProviderError, readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
3
- import { DEFAULT_SEARCH_COUNT, MAX_SEARCH_COUNT, buildSearchCacheKey, formatCliCommand, parseWebSearchTimeFilters, readCachedSearchPayload, readConfiguredSecretString, readPositiveIntegerParam, readProviderEnvValue, readStringParam, resolveSearchCacheTtlMs, resolveSearchCount, resolveSearchTimeoutSeconds, resolveSiteName, withSelfHostedWebSearchEndpoint, withTrustedWebSearchEndpoint, wrapWebContent, writeCachedSearchPayload } from "openclaw/plugin-sdk/provider-web-search";
4
+ import { DEFAULT_SEARCH_COUNT, MAX_SEARCH_COUNT, buildSearchCacheKey, formatCliCommand, parseWebSearchTimeFilters, readCachedSearchPayload, readConfiguredSecretString, readPositiveIntegerParam, readProviderEnvValue, readStringParam, resolveSearchCacheTtlMs, resolveSearchCount, resolveSearchTimeoutSeconds, resolveSiteName, withSelfHostedWebToolsEndpoint, withTrustedWebToolsEndpoint, wrapWebContent, writeCachedSearchPayload } from "openclaw/plugin-sdk/provider-web-search";
4
5
  import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
5
6
  import { assertHttpUrlTargetsPrivateNetwork, isBlockedHostnameOrIp, isPrivateIpAddress, resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
6
7
  //#region extensions/brave/src/brave-web-search-provider.shared.ts
@@ -171,7 +172,8 @@ function mapBraveLlmContextResults(data) {
171
172
  url: entry.url ?? "",
172
173
  title: entry.title ?? "",
173
174
  snippets: (entry.snippets ?? []).filter((snippet) => typeof snippet === "string" && snippet.length > 0),
174
- siteName: resolveSiteName(entry.url) || void 0
175
+ siteName: resolveSiteName(entry.url) || void 0,
176
+ published: data.sources?.[entry.url]?.age?.[3] || data.sources?.[entry.url]?.age?.[1] || void 0
175
177
  }));
176
178
  }
177
179
  //#endregion
@@ -200,18 +202,22 @@ function buildBraveEndpointUrl(params) {
200
202
  url.search = "";
201
203
  return url;
202
204
  }
203
- async function braveEndpointTargetsPrivateNetwork(url) {
205
+ async function braveEndpointTargetsPrivateNetwork(url, signal) {
204
206
  if (isBlockedHostnameOrIp(url.hostname)) return true;
205
207
  try {
206
- return (await resolvePinnedHostnameWithPolicy(url.hostname, { policy: {
207
- allowPrivateNetwork: true,
208
- allowRfc2544BenchmarkRange: true
209
- } })).addresses.every((address) => isPrivateIpAddress(address));
208
+ return (await resolvePinnedHostnameWithPolicy(url.hostname, {
209
+ signal,
210
+ policy: {
211
+ allowPrivateNetwork: true,
212
+ allowRfc2544BenchmarkRange: true
213
+ }
214
+ })).addresses.every((address) => isPrivateIpAddress(address));
210
215
  } catch {
216
+ signal?.throwIfAborted();
211
217
  return false;
212
218
  }
213
219
  }
214
- async function validateBraveBaseUrl(baseUrl) {
220
+ async function validateBraveBaseUrl(baseUrl, signal) {
215
221
  let parsed;
216
222
  try {
217
223
  parsed = new URL(baseUrl);
@@ -221,12 +227,13 @@ async function validateBraveBaseUrl(baseUrl) {
221
227
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("Brave Search base URL must use http:// or https://.");
222
228
  if (parsed.protocol === "http:") {
223
229
  await assertHttpUrlTargetsPrivateNetwork(parsed.toString(), {
230
+ signal,
224
231
  dangerouslyAllowPrivateNetwork: true,
225
232
  errorMessage: "Brave Search HTTP base URL must target a trusted private or loopback host. Use https:// for public hosts."
226
233
  });
227
234
  return "selfHosted";
228
235
  }
229
- return await braveEndpointTargetsPrivateNetwork(parsed) ? "selfHosted" : "strict";
236
+ return await braveEndpointTargetsPrivateNetwork(parsed, signal) ? "selfHosted" : "strict";
230
237
  }
231
238
  function missingBraveKeyPayload() {
232
239
  return {
@@ -257,9 +264,8 @@ async function runBraveJsonRequest(params, errorLabel) {
257
264
  params: Object.fromEntries(url.searchParams.entries())
258
265
  });
259
266
  const startedAt = Date.now();
260
- return (params.endpointMode === "selfHosted" ? withSelfHostedWebSearchEndpoint : withTrustedWebSearchEndpoint)({
267
+ return (params.endpointMode === "selfHosted" ? withSelfHostedWebToolsEndpoint : withTrustedWebToolsEndpoint)({
261
268
  url: url.toString(),
262
- timeoutSeconds: params.timeoutSeconds,
263
269
  signal: params.signal,
264
270
  init: {
265
271
  method: "GET",
@@ -268,7 +274,7 @@ async function runBraveJsonRequest(params, errorLabel) {
268
274
  "X-Subscription-Token": params.apiKey
269
275
  }
270
276
  }
271
- }, async (response) => {
277
+ }, async ({ response }) => {
272
278
  logBraveHttp(params.diagnostics, "response", {
273
279
  mode: params.mode,
274
280
  status: response.status,
@@ -315,7 +321,7 @@ async function runBraveWebSearch(params) {
315
321
  title: title ? wrapWebContent(title, "web_search") : "",
316
322
  url,
317
323
  description: description ? wrapWebContent(description, "web_search") : "",
318
- published: entry.age || void 0,
324
+ published: entry.page_age || void 0,
319
325
  siteName: resolveSiteName(url) || void 0
320
326
  };
321
327
  });
@@ -327,159 +333,175 @@ async function executeBraveSearch(args, searchConfig, options) {
327
333
  const braveConfig = resolveBraveConfig(searchConfig);
328
334
  const braveMode = resolveBraveMode(braveConfig);
329
335
  const braveBaseUrl = resolveBraveBaseUrl(braveConfig);
330
- const braveEndpointMode = await validateBraveBaseUrl(braveBaseUrl);
331
- const query = readStringParam(args, "query", { required: true });
332
- const count = readPositiveIntegerParam(args, "count", {
333
- max: MAX_SEARCH_COUNT,
334
- message: `count must be an integer from 1 to ${MAX_SEARCH_COUNT}.`
335
- }) ?? searchConfig?.maxResults ?? void 0;
336
- const country = normalizeBraveCountry(readStringParam(args, "country"));
337
- const language = readStringParam(args, "language");
338
- const search_lang = readStringParam(args, "search_lang");
339
- const ui_lang = readStringParam(args, "ui_lang");
340
- const normalizedLanguage = normalizeBraveLanguageParams({
341
- search_lang: search_lang || language,
342
- ui_lang
343
- });
344
- if (normalizedLanguage.invalidField === "search_lang") return {
345
- error: "invalid_search_lang",
346
- message: "search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.",
347
- docs: "https://docs.openclaw.ai/tools/web"
348
- };
349
- if (normalizedLanguage.invalidField === "ui_lang") return {
350
- error: "invalid_ui_lang",
351
- message: "ui_lang must be a language-region locale like 'en-US'.",
352
- docs: "https://docs.openclaw.ai/tools/web"
353
- };
354
- if (normalizedLanguage.ui_lang && braveMode === "llm-context") return {
355
- error: "unsupported_ui_lang",
356
- message: "ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
357
- docs: "https://docs.openclaw.ai/tools/web"
358
- };
359
- const rawFreshness = readStringParam(args, "freshness");
360
- const parsedTimeFilters = parseWebSearchTimeFilters({
361
- rawDateAfter: readStringParam(args, "date_after"),
362
- rawDateBefore: readStringParam(args, "date_before"),
363
- rawFreshness,
364
- freshnessProvider: "brave",
365
- invalidFreshnessMessage: "freshness must be day, week, month, or year.",
366
- invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
367
- invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
368
- invalidDateRangeMessage: "date_after must be before date_before."
336
+ const { signal, cleanup } = buildTimeoutAbortSignal({
337
+ timeoutMs: resolveSearchTimeoutSeconds(searchConfig) * 1e3,
338
+ signal: options?.signal,
339
+ operation: "brave.web_search",
340
+ url: braveBaseUrl
369
341
  });
370
- if ("error" in parsedTimeFilters) return parsedTimeFilters;
371
- const { freshness, dateAfter, dateBefore } = parsedTimeFilters;
372
- if (braveMode === "llm-context") {
373
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
374
- if (dateAfter && !dateBefore && dateAfter > today) return {
375
- error: "invalid_date_range",
376
- message: "date_after cannot be in the future for Brave llm-context mode.",
342
+ try {
343
+ signal?.throwIfAborted();
344
+ const braveEndpointMode = await validateBraveBaseUrl(braveBaseUrl, signal);
345
+ signal?.throwIfAborted();
346
+ const query = readStringParam(args, "query", { required: true });
347
+ const count = readPositiveIntegerParam(args, "count", {
348
+ max: MAX_SEARCH_COUNT,
349
+ message: `count must be an integer from 1 to ${MAX_SEARCH_COUNT}.`
350
+ }) ?? searchConfig?.maxResults ?? void 0;
351
+ const country = normalizeBraveCountry(readStringParam(args, "country"));
352
+ const language = readStringParam(args, "language");
353
+ const search_lang = readStringParam(args, "search_lang");
354
+ const ui_lang = readStringParam(args, "ui_lang");
355
+ const normalizedLanguage = normalizeBraveLanguageParams({
356
+ search_lang: search_lang || language,
357
+ ui_lang
358
+ });
359
+ if (normalizedLanguage.invalidField === "search_lang") return {
360
+ error: "invalid_search_lang",
361
+ message: "search_lang must be a Brave-supported language code like 'en', 'en-gb', 'zh-hans', or 'zh-hant'.",
377
362
  docs: "https://docs.openclaw.ai/tools/web"
378
363
  };
379
- if (dateBefore && !dateAfter) return {
380
- error: "unsupported_date_filter",
381
- message: "Brave llm-context mode requires date_after when date_before is set. Use a bounded date range or freshness.",
364
+ if (normalizedLanguage.invalidField === "ui_lang") return {
365
+ error: "invalid_ui_lang",
366
+ message: "ui_lang must be a language-region locale like 'en-US'.",
382
367
  docs: "https://docs.openclaw.ai/tools/web"
383
368
  };
384
- }
385
- const llmContextDateEnd = braveMode === "llm-context" && dateAfter ? dateBefore ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) : dateBefore;
386
- const cacheKey = buildSearchCacheKey(braveMode === "llm-context" ? [
387
- "brave",
388
- braveMode,
389
- braveBaseUrl,
390
- query,
391
- country,
392
- normalizedLanguage.search_lang,
393
- freshness,
394
- dateAfter,
395
- llmContextDateEnd
396
- ] : [
397
- "brave",
398
- braveMode,
399
- braveBaseUrl,
400
- query,
401
- resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
402
- country,
403
- normalizedLanguage.search_lang,
404
- normalizedLanguage.ui_lang,
405
- freshness,
406
- dateAfter,
407
- dateBefore
408
- ]);
409
- const diagnostics = { enabled: options?.diagnosticsEnabled === true };
410
- const cached = readCachedSearchPayload(cacheKey);
411
- if (cached) {
412
- logBraveHttp(diagnostics, "cache hit", {
369
+ if (normalizedLanguage.ui_lang && braveMode === "llm-context") return {
370
+ error: "unsupported_ui_lang",
371
+ message: "ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
372
+ docs: "https://docs.openclaw.ai/tools/web"
373
+ };
374
+ const rawFreshness = readStringParam(args, "freshness");
375
+ const rawDateAfter = readStringParam(args, "date_after");
376
+ const rawDateBefore = readStringParam(args, "date_before");
377
+ const parsedTimeFilters = parseWebSearchTimeFilters({
378
+ rawDateAfter,
379
+ rawDateBefore,
380
+ rawFreshness,
381
+ freshnessProvider: "brave",
382
+ invalidFreshnessMessage: "freshness must be day, week, month, or year.",
383
+ invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
384
+ invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
385
+ invalidDateRangeMessage: "date_after must be before date_before."
386
+ });
387
+ if ("error" in parsedTimeFilters) return parsedTimeFilters;
388
+ const { freshness, dateAfter, dateBefore } = parsedTimeFilters;
389
+ if (braveMode === "llm-context") {
390
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
391
+ if (dateAfter && !dateBefore && dateAfter > today) return {
392
+ error: "invalid_date_range",
393
+ message: "date_after cannot be in the future for Brave llm-context mode.",
394
+ docs: "https://docs.openclaw.ai/tools/web"
395
+ };
396
+ if (dateBefore && !dateAfter) return {
397
+ error: "unsupported_date_filter",
398
+ message: "Brave llm-context mode requires date_after when date_before is set. Use a bounded date range or freshness.",
399
+ docs: "https://docs.openclaw.ai/tools/web"
400
+ };
401
+ }
402
+ const llmContextDateEnd = braveMode === "llm-context" && dateAfter ? dateBefore ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) : dateBefore;
403
+ const cacheKey = buildSearchCacheKey(braveMode === "llm-context" ? [
404
+ "brave",
405
+ braveMode,
406
+ braveBaseUrl,
407
+ query,
408
+ country,
409
+ normalizedLanguage.search_lang,
410
+ freshness,
411
+ dateAfter,
412
+ llmContextDateEnd
413
+ ] : [
414
+ "brave",
415
+ braveMode,
416
+ braveBaseUrl,
417
+ query,
418
+ resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
419
+ country,
420
+ normalizedLanguage.search_lang,
421
+ normalizedLanguage.ui_lang,
422
+ freshness,
423
+ dateAfter,
424
+ dateBefore
425
+ ]);
426
+ const diagnostics = { enabled: options?.diagnosticsEnabled === true };
427
+ const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
428
+ const cached = readCachedSearchPayload(cacheKey, cacheTtlMs);
429
+ if (cached) {
430
+ logBraveHttp(diagnostics, "cache hit", {
431
+ mode: braveMode,
432
+ query,
433
+ cacheKey
434
+ });
435
+ return cached;
436
+ }
437
+ logBraveHttp(diagnostics, "cache miss", {
413
438
  mode: braveMode,
414
439
  query,
415
440
  cacheKey
416
441
  });
417
- return cached;
418
- }
419
- logBraveHttp(diagnostics, "cache miss", {
420
- mode: braveMode,
421
- query,
422
- cacheKey
423
- });
424
- const start = Date.now();
425
- const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
426
- const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
427
- const request = {
428
- baseUrl: braveBaseUrl,
429
- endpointMode: braveEndpointMode,
430
- query,
431
- apiKey,
432
- timeoutSeconds,
433
- diagnostics,
434
- signal: options?.signal,
435
- country: country ?? void 0,
436
- search_lang: normalizedLanguage.search_lang,
437
- freshness,
438
- dateAfter,
439
- dateBefore
440
- };
441
- const response = braveMode === "llm-context" ? {
442
- ...await runBraveLlmContextSearch(request),
443
- mode: "llm-context"
444
- } : {
445
- results: await runBraveWebSearch({
446
- ...request,
447
- count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
448
- ui_lang: normalizedLanguage.ui_lang
449
- }),
450
- mode: "web"
451
- };
452
- options?.signal?.throwIfAborted();
453
- const results = response.mode === "llm-context" ? response.results.map((entry) => ({
454
- title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
455
- url: entry.url,
456
- snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")),
457
- siteName: entry.siteName
458
- })) : response.results;
459
- const payload = {
460
- query,
461
- provider: "brave",
462
- ...response.mode === "llm-context" ? { mode: response.mode } : {},
463
- count: results.length,
464
- tookMs: Date.now() - start,
465
- externalContent: {
466
- untrusted: true,
467
- source: "web_search",
442
+ const start = Date.now();
443
+ const request = {
444
+ baseUrl: braveBaseUrl,
445
+ endpointMode: braveEndpointMode,
446
+ query,
447
+ apiKey,
448
+ diagnostics,
449
+ signal,
450
+ country: country ?? void 0,
451
+ search_lang: normalizedLanguage.search_lang,
452
+ freshness,
453
+ dateAfter,
454
+ dateBefore
455
+ };
456
+ const response = braveMode === "llm-context" ? {
457
+ ...await runBraveLlmContextSearch(request),
458
+ mode: "llm-context"
459
+ } : {
460
+ results: await runBraveWebSearch({
461
+ ...request,
462
+ count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
463
+ ui_lang: normalizedLanguage.ui_lang
464
+ }),
465
+ mode: "web"
466
+ };
467
+ signal?.throwIfAborted();
468
+ const results = response.mode === "llm-context" ? response.results.map((entry) => ({
469
+ title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
470
+ url: entry.url,
471
+ snippets: entry.snippets.map((snippet) => wrapWebContent(snippet, "web_search")),
472
+ siteName: entry.siteName,
473
+ published: entry.published
474
+ })) : response.results;
475
+ const payload = {
476
+ query,
468
477
  provider: "brave",
469
- wrapped: true
470
- },
471
- results,
472
- ...response.mode === "llm-context" ? { sources: response.sources } : {}
473
- };
474
- writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
475
- logBraveHttp(diagnostics, "cache write", {
476
- mode: response.mode,
477
- query,
478
- cacheKey,
479
- ttlMs: cacheTtlMs,
480
- count: results.length
481
- });
482
- return payload;
478
+ ...response.mode === "llm-context" ? { mode: response.mode } : {},
479
+ count: results.length,
480
+ tookMs: Date.now() - start,
481
+ externalContent: {
482
+ untrusted: true,
483
+ source: "web_search",
484
+ provider: "brave",
485
+ wrapped: true
486
+ },
487
+ results,
488
+ ...response.mode === "llm-context" ? { sources: response.sources } : {}
489
+ };
490
+ writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
491
+ logBraveHttp(diagnostics, "cache write", {
492
+ mode: response.mode,
493
+ query,
494
+ cacheKey,
495
+ ttlMs: cacheTtlMs,
496
+ count: results.length
497
+ });
498
+ return payload;
499
+ } catch (error) {
500
+ signal?.throwIfAborted();
501
+ throw error;
502
+ } finally {
503
+ cleanup();
504
+ }
483
505
  }
484
506
  //#endregion
485
507
  export { executeBraveSearch };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createBraveWebSearchProvider } from "./brave-web-search-provider-BbKJ74HP.js";
1
+ import { t as createBraveWebSearchProvider } from "./brave-web-search-provider-CY6mh6hm.js";
2
2
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
3
  //#region extensions/brave/index.ts
4
4
  /**
@@ -1,3 +1,3 @@
1
- import { t as createBraveWebSearchProvider } from "./brave-web-search-provider-BbKJ74HP.js";
1
+ import { t as createBraveWebSearchProvider } from "./brave-web-search-provider-CY6mh6hm.js";
2
2
  import "./web-search-provider.js";
3
3
  export { createBraveWebSearchProvider };
@@ -1,2 +1,2 @@
1
- import { t as createBraveWebSearchProvider } from "./brave-web-search-provider-BbKJ74HP.js";
1
+ import { t as createBraveWebSearchProvider } from "./brave-web-search-provider-CY6mh6hm.js";
2
2
  export { createBraveWebSearchProvider };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/brave-plugin",
3
- "version": "2026.9.1-beta.1",
3
+ "version": "2026.9.1",
4
4
  "description": "OpenClaw Brave Search provider plugin for web search.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,10 +18,10 @@
18
18
  "allowInvalidConfigRecovery": true
19
19
  },
20
20
  "compat": {
21
- "pluginApi": ">=2026.9.1-beta.1"
21
+ "pluginApi": ">=2026.9.1"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.9.1-beta.1"
24
+ "openclawVersion": "2026.9.1"
25
25
  },
26
26
  "release": {
27
27
  "publishToClawHub": true,
@@ -37,7 +37,7 @@
37
37
  "README.md"
38
38
  ],
39
39
  "peerDependencies": {
40
- "openclaw": ">=2026.9.1-beta.1"
40
+ "openclaw": ">=2026.9.1"
41
41
  },
42
42
  "peerDependenciesMeta": {
43
43
  "openclaw": {