feat(tools): add Exa as native web search provider extension#49319
feat(tools): add Exa as native web search provider extension#49319V-Gutierrez wants to merge 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR adds Exa as a native web search provider, correctly following the plugin-based extension architecture used by Key issues found:
Confidence Score: 3/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/exa/src/exa-search-provider.ts
Line: 191
Comment:
**`setUTCMonth` month-boundary overflow**
`Date.prototype.setUTCMonth(month - 1)` has a well-known edge case: when the current day-of-month is larger than the number of days in the target month, JavaScript silently overflows into the next month. Concretely, on March 31 this becomes:
```
date.setUTCMonth(1) // Feb has 28/29 days → overflows to March 2 or 3
```
so "last month" from March 31 resolves to `2026-03-02` or `2026-03-03` instead of `2026-02-28`. The date range would be only ~28 days, but offset by a full month, effectively returning wrong results.
The same bug exists in `src/agents/tools/web-search-core.ts` at line 1287 inside `resolveExaFreshnessStartDate`.
A safe fix is to clamp the day before setting the month:
```suggestion
} else if (freshness === "month") {
const targetMonth = date.getUTCMonth() - 1;
date.setUTCMonth(targetMonth);
// Clamp overflow (e.g. March 31 → Feb 28/29)
const expectedMonth = ((targetMonth % 12) + 12) % 12;
if (date.getUTCMonth() !== expectedMonth) {
date.setUTCDate(0); // last day of the overflowed-back month
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/agents/tools/web-search-core.ts
Line: 1999-2007
Comment:
**Configured `timeoutSeconds` not forwarded to Exa plugin**
All other provider dispatch paths in `runWebSearch` forward `params.timeoutSeconds` to their underlying request functions (e.g. Perplexity at line 1851, Gemini at line 1965, Brave at line 2020). When the Exa path delegates to the plugin here, it omits `timeoutSeconds`, so the plugin always falls back to its internal `DEFAULT_TIMEOUT_SECONDS = 30` regardless of what the user has set in `tools.web.search.timeoutSeconds`.
The plugin's `execute()` signature doesn't expose a `timeoutSeconds` arg, but `runExaSearch` does accept one. Consider surfacing this through the `searchConfig` context or the tool's args so the user-configured value is respected.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "fix: move mock-heavy..." |
| freshness: Type.Optional( | ||
| Type.String({ | ||
| description: "Time filter: day, week, month, year (also accepts pd/pw/pm/py).", | ||
| }), | ||
| ), | ||
| date_after: Type.Optional( | ||
| Type.String({ | ||
| description: "Only include results published after this date (YYYY-MM-DD or ISO datetime).", | ||
| }), | ||
| ), | ||
| date_before: Type.Optional( | ||
| Type.String({ | ||
| description: | ||
| "Only include results published before this date (YYYY-MM-DD or ISO datetime).", | ||
| }), | ||
| ), | ||
| type: Type.Optional( | ||
| Type.Union([Type.Literal("neural"), Type.Literal("keyword"), Type.Literal("auto")], { | ||
| description: "Exa search mode (neural, keyword, or auto). Default: auto.", | ||
| }), | ||
| ), | ||
| contents: Type.Optional( | ||
| Type.Object( | ||
| { | ||
| highlights: Type.Optional( | ||
| Type.Boolean({ description: "Include Exa highlights in results." }), | ||
| ), | ||
| text: Type.Optional(Type.Boolean({ description: "Include full text in results." })), | ||
| }, | ||
| { additionalProperties: false }, | ||
| ), | ||
| ), | ||
| }, | ||
| { additionalProperties: false }, | ||
| ); |
There was a problem hiding this comment.
Missing
freshness + date-range conflict validation
The ExaSearchSchema exposes freshness, date_after, and date_before as independent optional fields with no mutual-exclusivity constraint. This means a caller can supply both freshness="week" and date_after="2024-01-01" at the same time — the plugin silently accepts this and simply lets date_after win (line 252–254).
The core dispatch path in web-search-core.ts (lines 2403–2409) explicitly rejects this combination with a "conflicting_time_filters" error:
"freshness and date_after/date_before cannot be used together. Use either freshness …"
Because both paths are active in this PR, the same invalid input now produces different outcomes depending on which code path handles the request — a silent no-op on the plugin path vs. a clear error on the core path. Aligning the plugin's schema or execute handler to detect and reject the conflict would make behavior consistent and help users understand why their freshness filter isn't working.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/exa/src/exa-search-provider.ts
Line: 45-79
Comment:
**Missing `freshness` + date-range conflict validation**
The `ExaSearchSchema` exposes `freshness`, `date_after`, and `date_before` as independent optional fields with no mutual-exclusivity constraint. This means a caller can supply both `freshness="week"` and `date_after="2024-01-01"` at the same time — the plugin silently accepts this and simply lets `date_after` win (line 252–254).
The core dispatch path in `web-search-core.ts` (lines 2403–2409) explicitly rejects this combination with a `"conflicting_time_filters"` error:
```
"freshness and date_after/date_before cannot be used together. Use either freshness …"
```
Because both paths are active in this PR, the same invalid input now produces different outcomes depending on which code path handles the request — a silent no-op on the plugin path vs. a clear error on the core path. Aligning the plugin's schema or `execute` handler to detect and reject the conflict would make behavior consistent and help users understand why their freshness filter isn't working.
How can I resolve this? If you propose a fix, please make it concise.
src/agents/tools/web-search-core.ts
Outdated
| } from "./web-shared.js"; | ||
|
|
||
| const SEARCH_PROVIDERS = ["brave", "gemini", "grok", "kimi", "perplexity"] as const; | ||
| const SEARCH_PROVIDERS = ["brave", "gemini", "exa", "grok", "kimi", "perplexity"] as const; |
There was a problem hiding this comment.
Dual implementation of Exa logic across core and plugin
This PR adds "exa" to SEARCH_PROVIDERS here, and then adds a full inline Exa dispatch path to web-search-core.ts — including runExaSearch, resolveExaConfig, resolveExaApiKey, resolveExaType, resolveExaContents, normalizeExaFreshness, resolveExaFreshnessStartDate, resolveExaDescription, etc. (see grep hits at lines ~923–2588). This is in addition to the full plugin implementation in extensions/exa/src/exa-search-provider.ts.
firecrawl, which the PR cites as the architectural pattern to follow, exists only as a plugin — it has no inline dispatch in web-search-core.ts. Exa now has both.
The PR description states "zero modifications to the provider dispatch logic in web-search-core.ts beyond adding exa to the provider list" — but in practice hundreds of lines of Exa-specific dispatch logic were added. This creates a maintenance burden: any bug fix (e.g. in freshness-start-date calculation, date normalization, or content extraction) would need to be applied in two separate, independently-maintained implementations.
Consider whether the inline web-search-core.ts path is intentional (e.g. as a transition shim for the legacy code path) and, if so, documenting that explicitly, or whether the inline path can be removed in favour of the plugin-only dispatch.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/web-search-core.ts
Line: 25
Comment:
**Dual implementation of Exa logic across core and plugin**
This PR adds `"exa"` to `SEARCH_PROVIDERS` here, and then adds a full inline Exa dispatch path to `web-search-core.ts` — including `runExaSearch`, `resolveExaConfig`, `resolveExaApiKey`, `resolveExaType`, `resolveExaContents`, `normalizeExaFreshness`, `resolveExaFreshnessStartDate`, `resolveExaDescription`, etc. (see grep hits at lines ~923–2588). This is in addition to the full plugin implementation in `extensions/exa/src/exa-search-provider.ts`.
`firecrawl`, which the PR cites as the architectural pattern to follow, exists **only** as a plugin — it has no inline dispatch in `web-search-core.ts`. Exa now has both.
The PR description states "zero modifications to the provider dispatch logic in `web-search-core.ts` beyond adding `exa` to the provider list" — but in practice hundreds of lines of Exa-specific dispatch logic were added. This creates a maintenance burden: any bug fix (e.g. in freshness-start-date calculation, date normalization, or content extraction) would need to be applied in two separate, independently-maintained implementations.
Consider whether the inline `web-search-core.ts` path is intentional (e.g. as a transition shim for the legacy code path) and, if so, documenting that explicitly, or whether the inline path can be removed in favour of the plugin-only dispatch.
How can I resolve this? If you propose a fix, please make it concise.| function toIsoDateTime(value: unknown): string | undefined { | ||
| if (typeof value !== "string") { | ||
| return undefined; | ||
| } | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| return undefined; | ||
| } | ||
| if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { | ||
| const date = new Date(`${trimmed}T00:00:00.000Z`); | ||
| return Number.isNaN(date.valueOf()) ? undefined : date.toISOString(); | ||
| } | ||
| const date = new Date(trimmed); | ||
| if (Number.isNaN(date.valueOf())) { | ||
| return undefined; | ||
| } | ||
| return date.toISOString(); | ||
| } |
There was a problem hiding this comment.
Invalid date strings silently produce no date filter in the plugin
toIsoDateTime returns undefined when it receives an invalid date string (e.g. "not-a-date"). The execute handler at line 315–316 passes the raw args.date_after through toIsoDateTime and then forwards the result (possibly undefined) to runExaSearch. In runExaSearch, if (params.dateAfter) is then false, so the date filter is silently dropped.
The inline dispatch in web-search-core.ts (lines 2434–2440) validates the date string upfront with normalizeToIsoDate and returns a structured "invalid_date" error to the caller before the search ever runs.
This means:
- Core path:
date_after="not-a-date"→ explicit"invalid_date"error. - Plugin path:
date_after="not-a-date"→ search runs without the date filter, returning silently broader results.
Adding explicit validation (or at minimum logging a warning) in the plugin's execute block before calling runExaSearch would keep the two paths consistent and make the silent failure visible.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/exa/src/exa-search-provider.ts
Line: 165-182
Comment:
**Invalid date strings silently produce no date filter in the plugin**
`toIsoDateTime` returns `undefined` when it receives an invalid date string (e.g. `"not-a-date"`). The `execute` handler at line 315–316 passes the raw `args.date_after` through `toIsoDateTime` and then forwards the result (possibly `undefined`) to `runExaSearch`. In `runExaSearch`, `if (params.dateAfter)` is then `false`, so the date filter is silently dropped.
The inline dispatch in `web-search-core.ts` (lines 2434–2440) validates the date string upfront with `normalizeToIsoDate` and returns a structured `"invalid_date"` error to the caller before the search ever runs.
This means:
- **Core path**: `date_after="not-a-date"` → explicit `"invalid_date"` error.
- **Plugin path**: `date_after="not-a-date"` → search runs without the date filter, returning silently broader results.
Adding explicit validation (or at minimum logging a warning) in the plugin's `execute` block before calling `runExaSearch` would keep the two paths consistent and make the silent failure visible.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb19fdfb3a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/agents/tools/web-search-core.ts
Outdated
| } else if (freshness === "month") { | ||
| date.setUTCMonth(date.getUTCMonth() - 1); |
There was a problem hiding this comment.
Clamp Exa month freshness before subtracting a month
When freshness: "month" is used on the 29th–31st of a long month, setUTCMonth(date.getUTCMonth() - 1) can overflow back into the current month (2026-03-31 becomes 2026-03-03 in Node). That makes the Exa request's startPublishedDate only a few days old, so month-filtered searches near month-end return much newer results than requested.
Useful? React with 👍 / 👎.
src/agents/tools/web-search-core.ts
Outdated
| function resolveExaApiKey(exa?: ExaConfig): string | undefined { | ||
| const fromConfig = normalizeApiKey(exa?.apiKey); |
There was a problem hiding this comment.
Resolve Exa SecretRefs before treating the API key as empty
Exa is now exposed through onboarding, including --secret-input-mode ref, but this helper only accepts literal strings. In flows that read the raw config without an active runtime snapshot, a configured tools.web.search.exa.apiKey: { source: "env", ... } is normalized to empty here, so web_search reports missing_exa_api_key instead of honoring or surfacing the configured reference.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
openclaw/src/agents/tools/web-search-core.ts
Lines 2341 to 2345 in 1d26346
The Exa provider added in this commit explicitly accepts YYYY-MM-DD or ISO datetimes in extensions/exa/src/exa-search-provider.ts, but the main web_search path still validates Exa dates with normalizeToIsoDate(). As a result, calls like web_search({ date_after: "2024-06-01T12:00:00Z" }) are rejected with invalid_date before they ever reach the provider, so the finer-grained publication-date filtering exposed by the plugin is not actually available through the tool.
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/agents/tools/web-search-core.ts
Outdated
| const providers = resolvePluginWebSearchProviders({ bundledAllowlistCompat: true }); | ||
| const exaPlugin = providers.find((p) => p.id === "exa"); |
There was a problem hiding this comment.
Honor plugin disablement when dispatching Exa searches
If an admin disables the bundled Exa plugin via plugins.enabled=false, a restrictive plugins.allow, or plugins.entries.exa.enabled=false, web_search still reaches Exa here because it reloads bundled providers without passing the current config. I checked src/plugins/web-search-providers.ts:94-106, and those gates are only applied when config is provided. In practice, tools.web.search.provider: exa can keep making outbound Exa requests even after the plugin was explicitly disabled.
Useful? React with 👍 / 👎.
src/agents/tools/web-search-core.ts
Outdated
| if ( | ||
| ("highlights" in raw && typeof raw.highlights !== "boolean") || | ||
| ("text" in raw && typeof raw.text !== "boolean") | ||
| ) { | ||
| return "invalid"; | ||
| } | ||
|
|
||
| return Object.keys(next).length > 0 ? next : undefined; |
There was a problem hiding this comment.
Reject unknown Exa contents keys
This parser only validates the types of highlights and text, so a payload like contents: { summaries: true } falls through to undefined and the search proceeds. That bypasses the strict validation the new Exa plugin added for unknown contents fields, which makes malformed calls look successful and can silently drop a caller's intended overrides instead of returning invalid_exa_contents.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e617d3e56f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/agents/tools/web-search-core.ts
Outdated
| if (provider === "exa" && params.type !== undefined && !rawExaType) { | ||
| return jsonResult({ | ||
| error: "invalid_exa_type", | ||
| message: 'Exa type must be "neural", "keyword", or "auto".', | ||
| docs: "https://docs.openclaw.ai/tools/web", |
There was a problem hiding this comment.
Accept Exa's current search-mode enum
Exa's current /search API accepts fast, deep, deep-reasoning, and instant in addition to neural/auto, while keyword is no longer documented. Restricting type to neural|keyword|auto here means any web_search call (and the matching config values added in this patch) that uses a supported deep/fast mode is rejected with invalid_exa_type before the request is sent, so users cannot access the modes Exa currently exposes.
Useful? React with 👍 / 👎.
src/agents/tools/web-search-core.ts
Outdated
| exaContents: | ||
| exaContentsParam === "invalid" | ||
| ? undefined | ||
| : (exaContentsParam ?? resolveExaContents(exaConfig)), |
There was a problem hiding this comment.
Treat empty
contents as an explicit Exa override
When tools.web.search.exa.contents is set in config, passing contents: {} on an individual web_search call should disable content enrichment for that call, but readExaContentsParam({}) returns undefined and this fallback reapplies the config defaults. In practice there is no way to opt out per request once text or highlights are configured globally, so callers can unexpectedly pull full-page text into requests that explicitly passed an empty contents object.
Useful? React with 👍 / 👎.
| } else if (freshness === "week") { | ||
| date.setUTCDate(date.getUTCDate() - 7); | ||
| } else if (freshness === "month") { | ||
| date.setUTCMonth(date.getUTCMonth() - 1); |
There was a problem hiding this comment.
setUTCMonth month-boundary overflow
Date.prototype.setUTCMonth(month - 1) has a well-known edge case: when the current day-of-month is larger than the number of days in the target month, JavaScript silently overflows into the next month. Concretely, on March 31 this becomes:
date.setUTCMonth(1) // Feb has 28/29 days → overflows to March 2 or 3
so "last month" from March 31 resolves to 2026-03-02 or 2026-03-03 instead of 2026-02-28. The date range would be only ~28 days, but offset by a full month, effectively returning wrong results.
The same bug exists in src/agents/tools/web-search-core.ts at line 1287 inside resolveExaFreshnessStartDate.
A safe fix is to clamp the day before setting the month:
| date.setUTCMonth(date.getUTCMonth() - 1); | |
| } else if (freshness === "month") { | |
| const targetMonth = date.getUTCMonth() - 1; | |
| date.setUTCMonth(targetMonth); | |
| // Clamp overflow (e.g. March 31 → Feb 28/29) | |
| const expectedMonth = ((targetMonth % 12) + 12) % 12; | |
| if (date.getUTCMonth() !== expectedMonth) { | |
| date.setUTCDate(0); // last day of the overflowed-back month | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/exa/src/exa-search-provider.ts
Line: 191
Comment:
**`setUTCMonth` month-boundary overflow**
`Date.prototype.setUTCMonth(month - 1)` has a well-known edge case: when the current day-of-month is larger than the number of days in the target month, JavaScript silently overflows into the next month. Concretely, on March 31 this becomes:
```
date.setUTCMonth(1) // Feb has 28/29 days → overflows to March 2 or 3
```
so "last month" from March 31 resolves to `2026-03-02` or `2026-03-03` instead of `2026-02-28`. The date range would be only ~28 days, but offset by a full month, effectively returning wrong results.
The same bug exists in `src/agents/tools/web-search-core.ts` at line 1287 inside `resolveExaFreshnessStartDate`.
A safe fix is to clamp the day before setting the month:
```suggestion
} else if (freshness === "month") {
const targetMonth = date.getUTCMonth() - 1;
date.setUTCMonth(targetMonth);
// Clamp overflow (e.g. March 31 → Feb 28/29)
const expectedMonth = ((targetMonth % 12) + 12) % 12;
if (date.getUTCMonth() !== expectedMonth) {
date.setUTCDate(0); // last day of the overflowed-back month
}
```
How can I resolve this? If you propose a fix, please make it concise.
src/agents/tools/web-search-core.ts
Outdated
| const result = await tool.execute({ | ||
| query: params.query, | ||
| count: params.count, | ||
| freshness: params.freshness, | ||
| date_after: params.dateAfter, | ||
| date_before: params.dateBefore, | ||
| type: params.exaType, | ||
| contents: params.exaContents, | ||
| }); |
There was a problem hiding this comment.
Configured
timeoutSeconds not forwarded to Exa plugin
All other provider dispatch paths in runWebSearch forward params.timeoutSeconds to their underlying request functions (e.g. Perplexity at line 1851, Gemini at line 1965, Brave at line 2020). When the Exa path delegates to the plugin here, it omits timeoutSeconds, so the plugin always falls back to its internal DEFAULT_TIMEOUT_SECONDS = 30 regardless of what the user has set in tools.web.search.timeoutSeconds.
The plugin's execute() signature doesn't expose a timeoutSeconds arg, but runExaSearch does accept one. Consider surfacing this through the searchConfig context or the tool's args so the user-configured value is respected.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/web-search-core.ts
Line: 1999-2007
Comment:
**Configured `timeoutSeconds` not forwarded to Exa plugin**
All other provider dispatch paths in `runWebSearch` forward `params.timeoutSeconds` to their underlying request functions (e.g. Perplexity at line 1851, Gemini at line 1965, Brave at line 2020). When the Exa path delegates to the plugin here, it omits `timeoutSeconds`, so the plugin always falls back to its internal `DEFAULT_TIMEOUT_SECONDS = 30` regardless of what the user has set in `tools.web.search.timeoutSeconds`.
The plugin's `execute()` signature doesn't expose a `timeoutSeconds` arg, but `runExaSearch` does accept one. Consider surfacing this through the `searchConfig` context or the tool's args so the user-configured value is respected.
How can I resolve this? If you propose a fix, please make it concise.Implements Exa (exa.ai) as a bundled plugin extension following the same architecture used by brave, firecrawl, google, moonshot, perplexity, and xai. - New extension at extensions/exa/ with full provider implementation - Neural, keyword, and auto search types - Content extraction via highlights and text fields - Freshness filters with month-boundary overflow protection - Uses plugin-sdk helpers (resolveSearchTimeoutSeconds, buildSearchCacheKey, readCachedSearchPayload, normalizeFreshness, etc.) - Auto-detect order 25 (between gemini:20 and grok:30) - Config: EXA_API_KEY env or plugins.entries.exa.config.webSearch.apiKey - 16 pure tests (no vi.mock — compatible with extension CI runner) - Documentation updated in docs/tools/web.md Supersedes openclaw#29322 which used the legacy inline provider pattern.
e617d3e to
8202800
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 820280057f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| exa: z | ||
| .object({ | ||
| apiKey: SecretInputSchema.optional().register(sensitive), | ||
| }) |
There was a problem hiding this comment.
Map legacy Exa config through normalization
Adding tools.web.search.exa.apiKey here makes the legacy compatibility path look supported, but validateConfigObjectRaw() first runs normalizeLegacyWebSearchConfig(), and that helper only migrates brave/firecrawl/gemini/grok/kimi/perplexity. In configs like tools.web.search.provider: "exa" with tools.web.search.exa.apiKey, the new exa object is dropped during validation, so users following the advertised one-release shim end up with missing_exa_api_key at runtime even though their config parsed successfully.
Useful? React with 👍 / 👎.
| readConfiguredSecretString(exaConfig.apiKey, "plugins.entries.exa.config.webSearch.apiKey") ?? | ||
| readConfiguredSecretString( | ||
| (searchConfig as Record<string, unknown> | undefined)?.apiKey, | ||
| "tools.web.search.apiKey", | ||
| ) ?? |
There was a problem hiding this comment.
Stop reusing Brave's legacy API key for Exa
The fallback to tools.web.search.apiKey is Brave-specific (src/config/types.tools.ts marks it as the legacy Brave credential path), so reading it here makes Exa treat an old Brave key as a valid Exa credential. If someone switches tools.web.search.provider from Brave to Exa without adding plugins.entries.exa.config.webSearch.apiKey or EXA_API_KEY, web_search now sends the Brave token to Exa and fails with a provider 401 instead of returning the normal missing-key guidance.
Useful? React with 👍 / 👎.
| autoDetectOrder: 25, | ||
| credentialPath: "plugins.entries.exa.config.webSearch.apiKey", | ||
| inactiveSecretPaths: ["plugins.entries.exa.config.webSearch.apiKey"], |
There was a problem hiding this comment.
Register Exa API-key paths with the secret target registry
This new credential path is not added to src/secrets/target-registry-data.ts, which is what discoverConfigSecretTargets, secret audits/plans, and the secrets.resolve gateway API use to enumerate valid secret-bearing config fields. As shipped, runtime can consume plugins.entries.exa.config.webSearch.apiKey, but secret tooling cannot discover or target Exa web-search keys, so those flows silently miss the new provider.
Useful? React with 👍 / 👎.
…registry, legacy config normalization - Remove tools.web.search.apiKey fallback from Exa (Brave-specific path) - Register plugins.entries.exa.config.webSearch.apiKey in secret target registry - Add exa to legacy web search config normalization map
Review Feedback Addressed 🔧Pushed a follow-up commit (
Note on older review commentsMost Greptile/Codex comments reference Validation
cc @Takhoffman (authored the web-search provider extraction refactor this PR builds on) @steipete — would appreciate a review when you get a chance 🙏 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 533933e4bc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (params.dateAfter) { | ||
| body.startPublishedDate = params.dateAfter; | ||
| } | ||
| if (params.dateBefore) { | ||
| body.endPublishedDate = params.dateBefore; |
There was a problem hiding this comment.
Serialize Exa date filters as ISO datetimes
When callers use date_after or date_before, normalizeToIsoDate() only produces YYYY-MM-DD, and runExaSearch() forwards that string unchanged to Exa's startPublishedDate/endPublishedDate. I checked Exa's current Search reference, and those fields are documented as ISO 8601 date-time values, so this new date-filter feature now depends on undocumented coercion and can fail or silently skip the filter for any Exa search that uses explicit dates.
Useful? React with 👍 / 👎.
| docsUrl: "https://docs.openclaw.ai/tools/web", | ||
| autoDetectOrder: 25, | ||
| credentialPath: "plugins.entries.exa.config.webSearch.apiKey", |
There was a problem hiding this comment.
Move Exa ahead of Gemini in auto-detect precedence
If both EXA_API_KEY and GEMINI_API_KEY are present and no explicit provider is set, sortWebSearchProviders() will still pick Gemini first because Exa's new autoDetectOrder is 25 while Gemini remains 20. That makes the runtime disagree with the new precedence described in this patch, so users who add Exa alongside an existing Gemini key will continue to get Gemini unexpectedly until they set tools.web.search.provider manually.
Useful? React with 👍 / 👎.
| id: "plugins.entries.exa.config.webSearch.apiKey", | ||
| targetType: "plugins.entries.exa.config.webSearch.apiKey", | ||
| configFile: "openclaw.json", | ||
| pathPattern: "plugins.entries.exa.config.webSearch.apiKey", | ||
| secretShape: SECRET_INPUT_SHAPE, |
There was a problem hiding this comment.
Register the legacy Exa shim in secret tooling
This patch adds a one-release compatibility shim for tools.web.search.exa.*, but the registry addition here only covers plugins.entries.exa.config.webSearch.apiKey. As a result, secret discovery/audit/plan flows still cannot see tools.web.search.exa.apiKey, so users who keep the advertised legacy Exa path lose the secret-tooling support that the other web-search shims already provide.
Useful? React with 👍 / 👎.
|
I appreciate the earlier pass here. I'm closing this as a duplicate of #52617. #49319 built the strongest implementation groundwork, but it was no longer merge-safe after the rebase and plugin-boundary changes. The landed PR carried that path forward on top of current main while keeping the Exa-specific filters and plugin ownership model. The credit is preserved in the changelog trail and landing note. If I drew that line wrong, tell me and I can reopen review right away. |
Summary
Adds Exa as a native web search provider, implemented as a plugin extension following the same architecture used by
firecrawlandmoonshot.Features
date_after,date_before,freshness)tools.web.search.exa.apiKey) +EXA_API_KEYenv fallbackopenclaw onboard)Architecture
Registers via
api.registerWebSearchProvider()— zero modifications to the provider dispatch logic inweb-search-core.tsbeyond addingexato the provider list.Files Changed (18)
web-search-core.tstypes.tools.ts,zod-schema,schema.help,schema.labelsonboard-search.tsweb-search-providers.ts,registry.tsdocs/tools/web.mdTesting
Configuration
Or via environment:
EXA_API_KEY=exa-...