mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
Tools: add x_search via xAI Responses
This commit is contained in:
committed by
Peter Steinberger
parent
5ed8ee6832
commit
38e4b77e60
@@ -27,6 +27,9 @@ openclaw onboard --auth-choice xai-api-key
|
||||
}
|
||||
```
|
||||
|
||||
OpenClaw now uses the xAI Responses API as the bundled xAI transport. The same
|
||||
`XAI_API_KEY` can also power Grok-backed `web_search` and first-class `x_search`.
|
||||
|
||||
## Current bundled model catalog
|
||||
|
||||
OpenClaw now includes these xAI model families out of the box:
|
||||
@@ -52,9 +55,9 @@ openclaw config set tools.web.search.provider grok
|
||||
|
||||
- Auth is API-key only today. There is no xAI OAuth/device-code flow in OpenClaw yet.
|
||||
- `grok-4.20-multi-agent-experimental-beta-0304` is not supported on the normal xAI provider path because it requires a different upstream API surface than the standard OpenClaw xAI transport.
|
||||
- Native xAI server-side tools such as `x_search` and `code_execution` are not yet first-class model-provider features in the bundled plugin.
|
||||
|
||||
## Notes
|
||||
|
||||
- OpenClaw applies xAI-specific tool-schema and tool-call compatibility fixes automatically on the shared runner path.
|
||||
- `web_search` and `x_search` are exposed as OpenClaw tools. OpenClaw enables the specific xAI built-in it needs inside each tool request instead of attaching both search tools to every chat turn.
|
||||
- For the broader provider overview, see [Model providers](/providers/index).
|
||||
|
||||
@@ -12,6 +12,9 @@ OpenClaw supports Grok as a `web_search` provider, using xAI web-grounded
|
||||
responses to produce AI-synthesized answers backed by live search results
|
||||
with citations.
|
||||
|
||||
The same `XAI_API_KEY` can also power the built-in `x_search` tool for X
|
||||
(formerly Twitter) post search.
|
||||
|
||||
## Get an API key
|
||||
|
||||
<Steps>
|
||||
@@ -69,4 +72,5 @@ Provider-specific filters are not currently supported.
|
||||
## Related
|
||||
|
||||
- [Web Search overview](/tools/web) -- all providers and auto-detection
|
||||
- [x_search in Web Search](/tools/web#x_search) -- first-class X search via xAI
|
||||
- [Gemini Search](/tools/gemini-search) -- AI-synthesized answers via Google grounding
|
||||
|
||||
@@ -52,19 +52,19 @@ OpenClaw has three layers that work together:
|
||||
|
||||
These tools ship with OpenClaw and are available without installing any plugins:
|
||||
|
||||
| Tool | What it does | Page |
|
||||
| ---------------------------- | -------------------------------------------------------- | --------------------------------- |
|
||||
| `exec` / `process` | Run shell commands, manage background processes | [Exec](/tools/exec) |
|
||||
| `browser` | Control a Chromium browser (navigate, click, screenshot) | [Browser](/tools/browser) |
|
||||
| `web_search` / `web_fetch` | Search the web, fetch page content | [Web](/tools/web) |
|
||||
| `read` / `write` / `edit` | File I/O in the workspace | |
|
||||
| `apply_patch` | Multi-hunk file patches | [Apply Patch](/tools/apply-patch) |
|
||||
| `message` | Send messages across all channels | [Agent Send](/tools/agent-send) |
|
||||
| `canvas` | Drive node Canvas (present, eval, snapshot) | |
|
||||
| `nodes` | Discover and target paired devices | |
|
||||
| `cron` / `gateway` | Manage scheduled jobs, restart gateway | |
|
||||
| `image` / `image_generate` | Analyze or generate images | |
|
||||
| `sessions_*` / `agents_list` | Session management, sub-agents | [Sub-agents](/tools/subagents) |
|
||||
| Tool | What it does | Page |
|
||||
| --------------------------------------- | -------------------------------------------------------- | --------------------------------- |
|
||||
| `exec` / `process` | Run shell commands, manage background processes | [Exec](/tools/exec) |
|
||||
| `browser` | Control a Chromium browser (navigate, click, screenshot) | [Browser](/tools/browser) |
|
||||
| `web_search` / `x_search` / `web_fetch` | Search the web, search X posts, fetch page content | [Web](/tools/web) |
|
||||
| `read` / `write` / `edit` | File I/O in the workspace | |
|
||||
| `apply_patch` | Multi-hunk file patches | [Apply Patch](/tools/apply-patch) |
|
||||
| `message` | Send messages across all channels | [Agent Send](/tools/agent-send) |
|
||||
| `canvas` | Drive node Canvas (present, eval, snapshot) | |
|
||||
| `nodes` | Discover and target paired devices | |
|
||||
| `cron` / `gateway` | Manage scheduled jobs, restart gateway | |
|
||||
| `image` / `image_generate` | Analyze or generate images | |
|
||||
| `sessions_*` / `agents_list` | Session management, sub-agents | [Sub-agents](/tools/subagents) |
|
||||
|
||||
For image work, use `image` for analysis and `image_generate` for generation or editing. If you target `openai/*`, `google/*`, `fal/*`, or another non-default image provider, configure that provider's auth/API key first.
|
||||
|
||||
@@ -115,7 +115,7 @@ Use `group:*` shorthands in allow/deny lists:
|
||||
| `group:fs` | read, write, edit, apply_patch |
|
||||
| `group:sessions` | sessions_list, sessions_history, sessions_send, sessions_spawn, sessions_yield, subagents, session_status |
|
||||
| `group:memory` | memory_search, memory_get |
|
||||
| `group:web` | web_search, web_fetch |
|
||||
| `group:web` | web_search, x_search, web_fetch |
|
||||
| `group:ui` | browser, canvas |
|
||||
| `group:automation` | cron, gateway |
|
||||
| `group:messaging` | message |
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
summary: "web_search tool -- search the web with Brave, Firecrawl, Gemini, Grok, Kimi, Perplexity, or Tavily"
|
||||
read_when:
|
||||
- You want to enable or configure web_search
|
||||
- You need to choose a search provider
|
||||
- You want to understand auto-detection and provider fallback
|
||||
title: "Web Search"
|
||||
sidebarTitle: "Web Search"
|
||||
summary: "web_search, x_search, and web_fetch -- search the web, search X posts, or fetch page content"
|
||||
read_when:
|
||||
- You want to enable or configure web_search
|
||||
- You want to enable or configure x_search
|
||||
- You need to choose a search provider
|
||||
- You want to understand auto-detection and provider fallback
|
||||
---
|
||||
|
||||
# Web Search
|
||||
@@ -13,6 +14,10 @@ sidebarTitle: "Web Search"
|
||||
The `web_search` tool searches the web using your configured provider and
|
||||
returns results. Results are cached by query for 15 minutes (configurable).
|
||||
|
||||
OpenClaw also includes `x_search` for X (formerly Twitter) posts and
|
||||
`web_fetch` for lightweight URL fetching. In this phase, `web_fetch` stays
|
||||
local while `web_search` and `x_search` can use xAI Responses under the hood.
|
||||
|
||||
<Info>
|
||||
`web_search` is a lightweight HTTP tool, not browser automation. For
|
||||
JS-heavy sites or logins, use the [Web Browser](/tools/browser). For
|
||||
@@ -40,6 +45,12 @@ returns results. Results are cached by query for 15 minutes (configurable).
|
||||
await web_search({ query: "OpenClaw plugin SDK" });
|
||||
```
|
||||
|
||||
For X posts, use:
|
||||
|
||||
```javascript
|
||||
await x_search({ query: "dinner recipes" });
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -136,6 +147,9 @@ Provider-specific config (API keys, base URLs, modes) lives under
|
||||
`plugins.entries.<plugin>.config.webSearch.*`. See the provider pages for
|
||||
examples.
|
||||
|
||||
For `x_search`, configure `tools.web.x_search.*` directly. It uses the same
|
||||
`XAI_API_KEY` fallback as Grok web search.
|
||||
|
||||
### Storing API keys
|
||||
|
||||
<Tabs>
|
||||
@@ -195,6 +209,55 @@ examples.
|
||||
-- use their dedicated tools for advanced options.
|
||||
</Warning>
|
||||
|
||||
## x_search
|
||||
|
||||
`x_search` queries X (formerly Twitter) posts using xAI and returns
|
||||
AI-synthesized answers with citations. It accepts natural-language queries and
|
||||
optional structured filters. OpenClaw only enables the built-in xAI `x_search`
|
||||
tool on the request that serves this tool call.
|
||||
|
||||
### x_search config
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
enabled: true,
|
||||
apiKey: "xai-...", // optional if XAI_API_KEY is set
|
||||
model: "grok-4-1-fast-non-reasoning",
|
||||
inlineCitations: false,
|
||||
maxTurns: 2,
|
||||
timeoutSeconds: 30,
|
||||
cacheTtlMinutes: 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### x_search parameters
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------------------- | ------------------------------------------------------ |
|
||||
| `query` | Search query (required) |
|
||||
| `allowed_x_handles` | Restrict results to specific X handles |
|
||||
| `excluded_x_handles` | Exclude specific X handles |
|
||||
| `from_date` | Only include posts on or after this date (YYYY-MM-DD) |
|
||||
| `to_date` | Only include posts on or before this date (YYYY-MM-DD) |
|
||||
| `enable_image_understanding` | Let xAI inspect images attached to matching posts |
|
||||
| `enable_video_understanding` | Let xAI inspect videos attached to matching posts |
|
||||
|
||||
### x_search example
|
||||
|
||||
```javascript
|
||||
await x_search({
|
||||
query: "dinner recipes",
|
||||
allowed_x_handles: ["nytfood"],
|
||||
from_date: "2026-03-01",
|
||||
});
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```javascript
|
||||
@@ -223,13 +286,13 @@ await web_search({
|
||||
|
||||
## Tool profiles
|
||||
|
||||
If you use tool profiles or allowlists, add `web_search` or `group:web`:
|
||||
If you use tool profiles or allowlists, add `web_search`, `x_search`, or `group:web`:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
allow: ["web_search"],
|
||||
// or: allow: ["group:web"] (includes both web_search and web_fetch)
|
||||
allow: ["web_search", "x_search"],
|
||||
// or: allow: ["group:web"] (includes web_search, x_search, and web_fetch)
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -238,3 +301,4 @@ If you use tool profiles or allowlists, add `web_search` or `group:web`:
|
||||
|
||||
- [Web Fetch](/tools/web-fetch) -- fetch a URL and extract readable content
|
||||
- [Web Browser](/tools/browser) -- full browser automation for JS-heavy sites
|
||||
- [Grok Search](/tools/grok-search) -- Grok as the `web_search` provider
|
||||
|
||||
156
extensions/xai/src/x-search-shared.ts
Normal file
156
extensions/xai/src/x-search-shared.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { normalizeXaiModelId } from "openclaw/plugin-sdk/provider-models";
|
||||
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { extractXaiWebSearchContent, type XaiWebSearchResponse } from "./web-search-shared.js";
|
||||
|
||||
export const XAI_X_SEARCH_ENDPOINT = "https://api.x.ai/v1/responses";
|
||||
export const XAI_DEFAULT_X_SEARCH_MODEL = "grok-4-1-fast-non-reasoning";
|
||||
|
||||
export type XaiXSearchConfig = {
|
||||
apiKey?: unknown;
|
||||
model?: unknown;
|
||||
inlineCitations?: unknown;
|
||||
maxTurns?: unknown;
|
||||
};
|
||||
|
||||
export type XaiXSearchOptions = {
|
||||
query: string;
|
||||
allowedXHandles?: string[];
|
||||
excludedXHandles?: string[];
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
enableImageUnderstanding?: boolean;
|
||||
enableVideoUnderstanding?: boolean;
|
||||
};
|
||||
|
||||
export type XaiXSearchResult = {
|
||||
content: string;
|
||||
citations: string[];
|
||||
inlineCitations?: XaiWebSearchResponse["inline_citations"];
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function resolveXaiXSearchConfig(config?: Record<string, unknown>): XaiXSearchConfig {
|
||||
return isRecord(config) ? (config as XaiXSearchConfig) : {};
|
||||
}
|
||||
|
||||
export function resolveXaiXSearchModel(config?: Record<string, unknown>): string {
|
||||
const resolved = resolveXaiXSearchConfig(config);
|
||||
return typeof resolved.model === "string" && resolved.model.trim()
|
||||
? normalizeXaiModelId(resolved.model.trim())
|
||||
: XAI_DEFAULT_X_SEARCH_MODEL;
|
||||
}
|
||||
|
||||
export function resolveXaiXSearchInlineCitations(config?: Record<string, unknown>): boolean {
|
||||
return resolveXaiXSearchConfig(config).inlineCitations === true;
|
||||
}
|
||||
|
||||
export function resolveXaiXSearchMaxTurns(config?: Record<string, unknown>): number | undefined {
|
||||
const raw = resolveXaiXSearchConfig(config).maxTurns;
|
||||
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = Math.trunc(raw);
|
||||
return normalized > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function buildXSearchTool(options: XaiXSearchOptions): Record<string, unknown> {
|
||||
return {
|
||||
type: "x_search",
|
||||
...(options.allowedXHandles?.length ? { allowed_x_handles: options.allowedXHandles } : {}),
|
||||
...(options.excludedXHandles?.length ? { excluded_x_handles: options.excludedXHandles } : {}),
|
||||
...(options.fromDate ? { from_date: options.fromDate } : {}),
|
||||
...(options.toDate ? { to_date: options.toDate } : {}),
|
||||
...(options.enableImageUnderstanding ? { enable_image_understanding: true } : {}),
|
||||
...(options.enableVideoUnderstanding ? { enable_video_understanding: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildXaiXSearchPayload(params: {
|
||||
query: string;
|
||||
model: string;
|
||||
tookMs: number;
|
||||
content: string;
|
||||
citations: string[];
|
||||
inlineCitations?: XaiWebSearchResponse["inline_citations"];
|
||||
options?: XaiXSearchOptions;
|
||||
}): Record<string, unknown> {
|
||||
return {
|
||||
query: params.query,
|
||||
provider: "xai",
|
||||
model: params.model,
|
||||
tookMs: params.tookMs,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "x_search",
|
||||
provider: "xai",
|
||||
wrapped: true,
|
||||
},
|
||||
content: wrapWebContent(params.content, "web_search"),
|
||||
citations: params.citations,
|
||||
...(params.inlineCitations ? { inlineCitations: params.inlineCitations } : {}),
|
||||
...(params.options?.allowedXHandles?.length
|
||||
? { allowedXHandles: params.options.allowedXHandles }
|
||||
: {}),
|
||||
...(params.options?.excludedXHandles?.length
|
||||
? { excludedXHandles: params.options.excludedXHandles }
|
||||
: {}),
|
||||
...(params.options?.fromDate ? { fromDate: params.options.fromDate } : {}),
|
||||
...(params.options?.toDate ? { toDate: params.options.toDate } : {}),
|
||||
...(params.options?.enableImageUnderstanding ? { enableImageUnderstanding: true } : {}),
|
||||
...(params.options?.enableVideoUnderstanding ? { enableVideoUnderstanding: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function requestXaiXSearch(params: {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
timeoutSeconds: number;
|
||||
inlineCitations: boolean;
|
||||
maxTurns?: number;
|
||||
options: XaiXSearchOptions;
|
||||
}): Promise<XaiXSearchResult> {
|
||||
return await postTrustedWebToolsJson(
|
||||
{
|
||||
url: XAI_X_SEARCH_ENDPOINT,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
apiKey: params.apiKey,
|
||||
body: {
|
||||
model: params.model,
|
||||
input: [{ role: "user", content: params.options.query }],
|
||||
tools: [buildXSearchTool(params.options)],
|
||||
...(params.maxTurns ? { max_turns: params.maxTurns } : {}),
|
||||
},
|
||||
errorLabel: "xAI",
|
||||
},
|
||||
async (response) => {
|
||||
const data = (await response.json()) as XaiWebSearchResponse;
|
||||
const { text, annotationCitations } = extractXaiWebSearchContent(data);
|
||||
const citations =
|
||||
Array.isArray(data.citations) && data.citations.length > 0
|
||||
? data.citations
|
||||
: annotationCitations;
|
||||
return {
|
||||
content: text ?? "No response",
|
||||
citations,
|
||||
inlineCitations:
|
||||
params.inlineCitations && Array.isArray(data.inline_citations)
|
||||
? data.inline_citations
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
buildXSearchTool,
|
||||
buildXaiXSearchPayload,
|
||||
requestXaiXSearch,
|
||||
resolveXaiXSearchConfig,
|
||||
resolveXaiXSearchInlineCitations,
|
||||
resolveXaiXSearchMaxTurns,
|
||||
resolveXaiXSearchModel,
|
||||
XAI_DEFAULT_X_SEARCH_MODEL,
|
||||
} as const;
|
||||
@@ -5,6 +5,8 @@ import { createOpenClawTools } from "./openclaw-tools.js";
|
||||
|
||||
vi.mock("../plugins/tools.js", () => ({
|
||||
resolvePluginTools: () => [],
|
||||
copyPluginToolMeta: () => undefined,
|
||||
getPluginToolMeta: () => undefined,
|
||||
}));
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
||||
import { createSessionsYieldTool } from "./tools/sessions-yield-tool.js";
|
||||
import { createSubagentsTool } from "./tools/subagents-tool.js";
|
||||
import { createTtsTool } from "./tools/tts-tool.js";
|
||||
import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
|
||||
import { createWebFetchTool, createWebSearchTool, createXSearchTool } from "./tools/web-tools.js";
|
||||
import { resolveWorkspaceRoot } from "./workspace-dir.js";
|
||||
|
||||
type OpenClawToolsDeps = {
|
||||
@@ -155,6 +155,10 @@ export function createOpenClawTools(
|
||||
sandboxed: options?.sandboxed,
|
||||
runtimeWebSearch: runtimeWebTools?.search,
|
||||
});
|
||||
const xSearchTool = createXSearchTool({
|
||||
config: options?.config,
|
||||
runtimeXSearch: runtimeWebTools?.xSearch,
|
||||
});
|
||||
const webFetchTool = createWebFetchTool({
|
||||
config: options?.config,
|
||||
sandboxed: options?.sandboxed,
|
||||
@@ -251,6 +255,7 @@ export function createOpenClawTools(
|
||||
sandboxed: options?.sandboxed,
|
||||
}),
|
||||
...(webSearchTool ? [webSearchTool] : []),
|
||||
...(xSearchTool ? [xSearchTool] : []),
|
||||
...(webFetchTool ? [webFetchTool] : []),
|
||||
...(imageTool ? [imageTool] : []),
|
||||
...(pdfTool ? [pdfTool] : []),
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
|
||||
const mockedModuleIds = [
|
||||
"../plugins/tools.js",
|
||||
"../gateway/call.js",
|
||||
"./tools/agents-list-tool.js",
|
||||
"./tools/canvas-tool.js",
|
||||
"./tools/cron-tool.js",
|
||||
"./tools/gateway-tool.js",
|
||||
"./tools/image-generate-tool.js",
|
||||
"./tools/image-tool.js",
|
||||
"./tools/message-tool.js",
|
||||
"./tools/nodes-tool.js",
|
||||
"./tools/pdf-tool.js",
|
||||
"./tools/session-status-tool.js",
|
||||
"./tools/sessions-history-tool.js",
|
||||
"./tools/sessions-list-tool.js",
|
||||
"./tools/sessions-send-tool.js",
|
||||
"./tools/sessions-spawn-tool.js",
|
||||
"./tools/sessions-yield-tool.js",
|
||||
"./tools/subagents-tool.js",
|
||||
"./tools/tts-tool.js",
|
||||
] as const;
|
||||
|
||||
vi.mock("../plugins/tools.js", () => ({
|
||||
resolvePluginTools: () => [],
|
||||
copyPluginToolMeta: () => undefined,
|
||||
getPluginToolMeta: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
@@ -123,6 +147,12 @@ describe("openclaw tools runtime web metadata wiring", () => {
|
||||
secretsRuntime.clearSecretsRuntimeSnapshot();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
for (const id of mockedModuleIds) {
|
||||
vi.doUnmock(id);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses runtime-selected provider when higher-precedence provider ref is unresolved", async () => {
|
||||
const snapshot = await prepareAndActivate({
|
||||
config: asConfig({
|
||||
@@ -204,4 +234,52 @@ describe("openclaw tools runtime web metadata wiring", () => {
|
||||
expect(mockFetch.mock.calls[0]?.[0]).toBe("https://example.com/runtime-off");
|
||||
expect(String(mockFetch.mock.calls[0]?.[0])).not.toContain("api.firecrawl.dev");
|
||||
});
|
||||
|
||||
it("resolves x_search SecretRef from the active runtime snapshot", async () => {
|
||||
const snapshot = await prepareAndActivate({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_RUNTIME_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
X_SEARCH_RUNTIME_REF: "x-search-runtime-key",
|
||||
},
|
||||
});
|
||||
|
||||
expect(snapshot.webTools.xSearch.active).toBe(true);
|
||||
expect(snapshot.webTools.xSearch.apiKeySource).toBe("secretRef");
|
||||
|
||||
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
output_text: "runtime x search ok",
|
||||
citations: ["https://x.com/openclaw/status/1"],
|
||||
}),
|
||||
} as Response),
|
||||
);
|
||||
global.fetch = withFetchPreconnect(mockFetch);
|
||||
|
||||
const xSearch = findTool("x_search", snapshot.config);
|
||||
const result = await xSearch.execute("call-runtime-x-search", {
|
||||
query: "runtime search",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
expect(String(mockFetch.mock.calls[0]?.[0])).toContain("api.x.ai/v1/responses");
|
||||
const request = mockFetch.mock.calls[0]?.[1] as RequestInit | undefined;
|
||||
const body = JSON.parse(typeof request?.body === "string" ? request.body : "{}") as {
|
||||
tools?: Array<Record<string, unknown>>;
|
||||
};
|
||||
expect(body.tools).toEqual([{ type: "x_search" }]);
|
||||
expect((result.details as { citations?: string[] }).citations).toEqual([
|
||||
"https://x.com/openclaw/status/1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,6 +158,7 @@ const TRUSTED_TOOL_RESULT_MEDIA = new Set([
|
||||
"tts",
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"x_search",
|
||||
"write",
|
||||
]);
|
||||
const HTTP_URL_RE = /^https?:\/\//i;
|
||||
|
||||
@@ -458,7 +458,7 @@ describe("createOpenClawCodingTools", () => {
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expect(xaiTools.some((tool) => tool.name === "web_search")).toBe(false);
|
||||
expect(xaiTools.some((tool) => tool.name === "web_search")).toBe(true);
|
||||
for (const tool of xaiTools) {
|
||||
const violations = findUnsupportedSchemaKeywords(tool.parameters, `${tool.name}.parameters`);
|
||||
expect(
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("applyModelProviderToolPolicy", () => {
|
||||
expect(toolNames(filtered)).toEqual(["read", "web_search", "exec"]);
|
||||
});
|
||||
|
||||
it("removes web_search for OpenRouter xAI model ids", () => {
|
||||
it("keeps web_search for OpenRouter xAI model ids so OpenClaw tool routing stays authoritative", () => {
|
||||
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
|
||||
modelCompat: {
|
||||
toolSchemaProfile: XAI_TOOL_SCHEMA_PROFILE,
|
||||
@@ -34,10 +34,10 @@ describe("applyModelProviderToolPolicy", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(toolNames(filtered)).toEqual(["read", "exec"]);
|
||||
expect(toolNames(filtered)).toEqual(["read", "web_search", "exec"]);
|
||||
});
|
||||
|
||||
it("removes web_search for direct xai-capable models too", () => {
|
||||
it("keeps web_search for direct xai-capable models too", () => {
|
||||
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
|
||||
modelCompat: {
|
||||
toolSchemaProfile: XAI_TOOL_SCHEMA_PROFILE,
|
||||
@@ -45,6 +45,6 @@ describe("applyModelProviderToolPolicy", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(toolNames(filtered)).toEqual(["read", "exec"]);
|
||||
expect(toolNames(filtered)).toEqual(["read", "web_search", "exec"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,6 @@ function isOpenAIProvider(provider?: string) {
|
||||
const TOOL_DENY_BY_MESSAGE_PROVIDER: Readonly<Record<string, readonly string[]>> = {
|
||||
voice: ["tts"],
|
||||
};
|
||||
const TOOL_DENY_FOR_XAI_PROVIDERS = new Set(["web_search"]);
|
||||
const MEMORY_FLUSH_ALLOWED_TOOL_NAMES = new Set(["read", "write"]);
|
||||
|
||||
function normalizeMessageProvider(messageProvider?: string): string | undefined {
|
||||
@@ -95,12 +94,8 @@ function applyModelProviderToolPolicy(
|
||||
tools: AnyAgentTool[],
|
||||
params?: { modelCompat?: ModelCompatConfig },
|
||||
): AnyAgentTool[] {
|
||||
if (!hasNativeWebSearchTool(params?.modelCompat)) {
|
||||
return tools;
|
||||
}
|
||||
// Models with a native web_search tool cannot receive OpenClaw's
|
||||
// web_search at the same time or the request will collide.
|
||||
return tools.filter((tool) => !TOOL_DENY_FOR_XAI_PROVIDERS.has(tool.name));
|
||||
void params;
|
||||
return tools;
|
||||
}
|
||||
|
||||
function isApplyPatchAllowedForModel(params: {
|
||||
|
||||
@@ -47,6 +47,7 @@ function buildCommonSystemParams(workspaceDir: string) {
|
||||
"memory_search",
|
||||
"memory_get",
|
||||
"web_search",
|
||||
"x_search",
|
||||
"web_fetch",
|
||||
];
|
||||
const toolSummaries = buildToolSummaryMap(
|
||||
@@ -158,6 +159,7 @@ function buildToolRichSystemPrompt(params: {
|
||||
"memory_search",
|
||||
"memory_get",
|
||||
"web_search",
|
||||
"x_search",
|
||||
"web_fetch",
|
||||
].map((name) => ({ ...createStubTool(name), description: `${name} tool` }));
|
||||
return buildEmbeddedSystemPrompt({
|
||||
|
||||
@@ -233,7 +233,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
ls: "List directory contents",
|
||||
exec: "Run shell commands (pty available for TTY-required CLIs)",
|
||||
process: "Manage background exec sessions",
|
||||
web_search: "Search the web (Brave API)",
|
||||
web_search: "Search the web",
|
||||
x_search: "Search X (formerly Twitter) posts with xAI",
|
||||
web_fetch: "Fetch and extract readable content from a URL",
|
||||
// Channel docking: add login tools here when a channel needs interactive linking.
|
||||
browser: "Control web browser",
|
||||
|
||||
@@ -2,10 +2,11 @@ import { describe, expect, it } from "vitest";
|
||||
import { resolveCoreToolProfilePolicy } from "./tool-catalog.js";
|
||||
|
||||
describe("tool-catalog", () => {
|
||||
it("includes web_search and web_fetch in the coding profile policy", () => {
|
||||
it("includes web_search, x_search, and web_fetch in the coding profile policy", () => {
|
||||
const policy = resolveCoreToolProfilePolicy("coding");
|
||||
expect(policy).toBeDefined();
|
||||
expect(policy!.allow).toContain("web_search");
|
||||
expect(policy!.allow).toContain("x_search");
|
||||
expect(policy!.allow).toContain("web_fetch");
|
||||
expect(policy!.allow).toContain("image_generate");
|
||||
});
|
||||
|
||||
@@ -97,6 +97,14 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [
|
||||
profiles: ["coding"],
|
||||
includeInOpenClawGroup: true,
|
||||
},
|
||||
{
|
||||
id: "x_search",
|
||||
label: "x_search",
|
||||
description: "Search X posts",
|
||||
sectionId: "web",
|
||||
profiles: ["coding"],
|
||||
includeInOpenClawGroup: true,
|
||||
},
|
||||
{
|
||||
id: "memory_search",
|
||||
label: "memory_search",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { createWebFetchTool, extractReadableContent, fetchFirecrawlContent } from "./web-fetch.js";
|
||||
export { createWebSearchTool } from "./web-search.js";
|
||||
export { createXSearchTool } from "./x-search.js";
|
||||
|
||||
128
src/agents/tools/x-search.test.ts
Normal file
128
src/agents/tools/x-search.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import { createXSearchTool } from "./x-search.js";
|
||||
|
||||
function installXSearchFetch(payload?: Record<string, unknown>) {
|
||||
const mockFetch = vi.fn((_input?: unknown, _init?: unknown) =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
payload ?? {
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
content: [
|
||||
{
|
||||
type: "output_text",
|
||||
text: "Found X posts",
|
||||
annotations: [{ type: "url_citation", url: "https://x.com/openclaw/status/1" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
citations: ["https://x.com/openclaw/status/1"],
|
||||
},
|
||||
),
|
||||
} as Response),
|
||||
);
|
||||
global.fetch = withFetchPreconnect(mockFetch);
|
||||
return mockFetch;
|
||||
}
|
||||
|
||||
function parseFirstRequestBody(mockFetch: ReturnType<typeof installXSearchFetch>) {
|
||||
const request = mockFetch.mock.calls[0]?.[1] as RequestInit | undefined;
|
||||
const requestBody = request?.body;
|
||||
return JSON.parse(typeof requestBody === "string" ? requestBody : "{}") as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("x_search tool", () => {
|
||||
it("enables x_search when runtime metadata marks an xAI key active", () => {
|
||||
const tool = createXSearchTool({
|
||||
config: {},
|
||||
runtimeXSearch: {
|
||||
active: true,
|
||||
apiKeySource: "env",
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(tool?.name).toBe("x_search");
|
||||
});
|
||||
|
||||
it("uses the xAI Responses x_search tool with structured filters", async () => {
|
||||
const mockFetch = installXSearchFetch();
|
||||
const tool = createXSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: "xai-config-test", // pragma: allowlist secret
|
||||
model: "grok-4-1-fast-non-reasoning",
|
||||
maxTurns: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await tool?.execute?.("x-search:1", {
|
||||
query: "dinner recipes",
|
||||
allowed_x_handles: ["openclaw"],
|
||||
excluded_x_handles: ["spam"],
|
||||
from_date: "2026-03-01",
|
||||
to_date: "2026-03-20",
|
||||
enable_image_understanding: true,
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
expect(String(mockFetch.mock.calls[0]?.[0])).toContain("api.x.ai/v1/responses");
|
||||
const body = parseFirstRequestBody(mockFetch);
|
||||
expect(body.model).toBe("grok-4-1-fast-non-reasoning");
|
||||
expect(body.max_turns).toBe(2);
|
||||
expect(body.tools).toEqual([
|
||||
{
|
||||
type: "x_search",
|
||||
allowed_x_handles: ["openclaw"],
|
||||
excluded_x_handles: ["spam"],
|
||||
from_date: "2026-03-01",
|
||||
to_date: "2026-03-20",
|
||||
enable_image_understanding: true,
|
||||
},
|
||||
]);
|
||||
expect((result?.details as { citations?: string[] } | undefined)?.citations).toEqual([
|
||||
"https://x.com/openclaw/status/1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects invalid date ordering before calling xAI", async () => {
|
||||
const mockFetch = installXSearchFetch();
|
||||
const tool = createXSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: "xai-config-test", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
tool?.execute?.("x-search:bad-dates", {
|
||||
query: "dinner recipes",
|
||||
from_date: "2026-03-20",
|
||||
to_date: "2026-03-01",
|
||||
}),
|
||||
).rejects.toThrow(/from_date must be on or before to_date/i);
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
231
src/agents/tools/x-search.ts
Normal file
231
src/agents/tools/x-search.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
__testing as xaiXSearchTesting,
|
||||
buildXaiXSearchPayload,
|
||||
requestXaiXSearch,
|
||||
resolveXaiXSearchInlineCitations,
|
||||
resolveXaiXSearchMaxTurns,
|
||||
resolveXaiXSearchModel,
|
||||
type XaiXSearchOptions,
|
||||
} from "../../../extensions/xai/src/x-search-shared.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { RuntimeWebXSearchMetadata } from "../../secrets/runtime-web-tools.types.js";
|
||||
import { jsonResult, readStringArrayParam, readStringParam, ToolInputError } from "./common.js";
|
||||
import {
|
||||
readConfiguredSecretString,
|
||||
readProviderEnvValue,
|
||||
SEARCH_CACHE,
|
||||
} from "./web-search-provider-common.js";
|
||||
import { readCache, resolveCacheTtlMs, resolveTimeoutSeconds, writeCache } from "./web-shared.js";
|
||||
|
||||
type XSearchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
? Web extends { x_search?: infer XSearch }
|
||||
? XSearch
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
function resolveXSearchConfig(cfg?: OpenClawConfig): XSearchConfig {
|
||||
const xSearch = cfg?.tools?.web?.x_search;
|
||||
if (!xSearch || typeof xSearch !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return xSearch as XSearchConfig;
|
||||
}
|
||||
|
||||
function resolveXSearchEnabled(params: {
|
||||
config?: XSearchConfig;
|
||||
runtimeXSearch?: RuntimeWebXSearchMetadata;
|
||||
}): boolean {
|
||||
if (params.config?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
if (params.runtimeXSearch?.active) {
|
||||
return true;
|
||||
}
|
||||
const configuredApiKey = readConfiguredSecretString(
|
||||
params.config?.apiKey,
|
||||
"tools.web.x_search.apiKey",
|
||||
);
|
||||
return Boolean(configuredApiKey || readProviderEnvValue(["XAI_API_KEY"]));
|
||||
}
|
||||
|
||||
function resolveXSearchApiKey(config?: XSearchConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(config?.apiKey, "tools.web.x_search.apiKey") ??
|
||||
readProviderEnvValue(["XAI_API_KEY"])
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeOptionalIsoDate(value: string | undefined, label: string): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
||||
throw new ToolInputError(`${label} must use YYYY-MM-DD`);
|
||||
}
|
||||
const [year, month, day] = trimmed.split("-").map((entry) => Number.parseInt(entry, 10));
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
if (
|
||||
date.getUTCFullYear() !== year ||
|
||||
date.getUTCMonth() !== month - 1 ||
|
||||
date.getUTCDate() !== day
|
||||
) {
|
||||
throw new ToolInputError(`${label} must be a valid calendar date`);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function buildXSearchCacheKey(params: {
|
||||
query: string;
|
||||
model: string;
|
||||
inlineCitations: boolean;
|
||||
maxTurns?: number;
|
||||
options: Omit<XaiXSearchOptions, "query">;
|
||||
}) {
|
||||
return JSON.stringify([
|
||||
"x_search",
|
||||
params.model,
|
||||
params.query,
|
||||
params.inlineCitations,
|
||||
params.maxTurns ?? null,
|
||||
params.options.allowedXHandles ?? null,
|
||||
params.options.excludedXHandles ?? null,
|
||||
params.options.fromDate ?? null,
|
||||
params.options.toDate ?? null,
|
||||
params.options.enableImageUnderstanding ?? false,
|
||||
params.options.enableVideoUnderstanding ?? false,
|
||||
]);
|
||||
}
|
||||
|
||||
export function createXSearchTool(options?: {
|
||||
config?: OpenClawConfig;
|
||||
runtimeXSearch?: RuntimeWebXSearchMetadata;
|
||||
}) {
|
||||
const xSearchConfig = resolveXSearchConfig(options?.config);
|
||||
if (!resolveXSearchEnabled({ config: xSearchConfig, runtimeXSearch: options?.runtimeXSearch })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: "X Search",
|
||||
name: "x_search",
|
||||
description:
|
||||
"Search X (formerly Twitter) using xAI. Returns AI-synthesized answers with citations from real-time X post search.",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "X search query string." }),
|
||||
allowed_x_handles: Type.Optional(
|
||||
Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: "Only include posts from these X handles.",
|
||||
}),
|
||||
),
|
||||
excluded_x_handles: Type.Optional(
|
||||
Type.Array(Type.String({ minLength: 1 }), {
|
||||
description: "Exclude posts from these X handles.",
|
||||
}),
|
||||
),
|
||||
from_date: Type.Optional(
|
||||
Type.String({ description: "Only include posts on or after this date (YYYY-MM-DD)." }),
|
||||
),
|
||||
to_date: Type.Optional(
|
||||
Type.String({ description: "Only include posts on or before this date (YYYY-MM-DD)." }),
|
||||
),
|
||||
enable_image_understanding: Type.Optional(
|
||||
Type.Boolean({ description: "Allow xAI to inspect images attached to matching posts." }),
|
||||
),
|
||||
enable_video_understanding: Type.Optional(
|
||||
Type.Boolean({ description: "Allow xAI to inspect videos attached to matching posts." }),
|
||||
),
|
||||
}),
|
||||
execute: async (_toolCallId: string, args: Record<string, unknown>) => {
|
||||
const apiKey = resolveXSearchApiKey(xSearchConfig);
|
||||
if (!apiKey) {
|
||||
return jsonResult({
|
||||
error: "missing_xai_api_key",
|
||||
message:
|
||||
"x_search needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure tools.web.x_search.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
}
|
||||
|
||||
const query = readStringParam(args, "query", { required: true });
|
||||
const allowedXHandles = readStringArrayParam(args, "allowed_x_handles");
|
||||
const excludedXHandles = readStringArrayParam(args, "excluded_x_handles");
|
||||
const fromDate = normalizeOptionalIsoDate(readStringParam(args, "from_date"), "from_date");
|
||||
const toDate = normalizeOptionalIsoDate(readStringParam(args, "to_date"), "to_date");
|
||||
if (fromDate && toDate && fromDate > toDate) {
|
||||
throw new ToolInputError("from_date must be on or before to_date");
|
||||
}
|
||||
|
||||
const xSearchOptions: XaiXSearchOptions = {
|
||||
query,
|
||||
allowedXHandles,
|
||||
excludedXHandles,
|
||||
fromDate,
|
||||
toDate,
|
||||
enableImageUnderstanding: args.enable_image_understanding === true,
|
||||
enableVideoUnderstanding: args.enable_video_understanding === true,
|
||||
};
|
||||
const xSearchConfigRecord = xSearchConfig as Record<string, unknown> | undefined;
|
||||
const model = resolveXaiXSearchModel(xSearchConfigRecord);
|
||||
const inlineCitations = resolveXaiXSearchInlineCitations(xSearchConfigRecord);
|
||||
const maxTurns = resolveXaiXSearchMaxTurns(xSearchConfigRecord);
|
||||
const cacheKey = buildXSearchCacheKey({
|
||||
query,
|
||||
model,
|
||||
inlineCitations,
|
||||
maxTurns,
|
||||
options: {
|
||||
allowedXHandles,
|
||||
excludedXHandles,
|
||||
fromDate,
|
||||
toDate,
|
||||
enableImageUnderstanding: xSearchOptions.enableImageUnderstanding,
|
||||
enableVideoUnderstanding: xSearchOptions.enableVideoUnderstanding,
|
||||
},
|
||||
});
|
||||
const cached = readCache(SEARCH_CACHE, cacheKey);
|
||||
if (cached) {
|
||||
return jsonResult({ ...cached.value, cached: true });
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const result = await requestXaiXSearch({
|
||||
apiKey,
|
||||
model,
|
||||
timeoutSeconds: resolveTimeoutSeconds(xSearchConfig?.timeoutSeconds, 30),
|
||||
inlineCitations,
|
||||
maxTurns,
|
||||
options: xSearchOptions,
|
||||
});
|
||||
const payload = buildXaiXSearchPayload({
|
||||
query,
|
||||
model,
|
||||
tookMs: Date.now() - startedAt,
|
||||
content: result.content,
|
||||
citations: result.citations,
|
||||
inlineCitations: result.inlineCitations,
|
||||
options: xSearchOptions,
|
||||
});
|
||||
writeCache(
|
||||
SEARCH_CACHE,
|
||||
cacheKey,
|
||||
payload,
|
||||
resolveCacheTtlMs(xSearchConfig?.cacheTtlMinutes, 15),
|
||||
);
|
||||
return jsonResult(payload);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
buildXSearchCacheKey,
|
||||
normalizeOptionalIsoDate,
|
||||
resolveXSearchApiKey,
|
||||
resolveXSearchConfig,
|
||||
resolveXSearchEnabled,
|
||||
...xaiXSearchTesting,
|
||||
} as const;
|
||||
@@ -57,10 +57,12 @@ type GatewaySecretsResolveResult = {
|
||||
const WEB_RUNTIME_SECRET_TARGET_ID_PREFIXES = [
|
||||
"tools.web.search",
|
||||
"tools.web.fetch.firecrawl",
|
||||
"tools.web.x_search",
|
||||
] as const;
|
||||
const WEB_RUNTIME_SECRET_PATH_PREFIXES = [
|
||||
"tools.web.search.",
|
||||
"tools.web.fetch.firecrawl.",
|
||||
"tools.web.x_search.",
|
||||
] as const;
|
||||
|
||||
function normalizeCommandSecretResolutionMode(
|
||||
@@ -106,6 +108,10 @@ function classifyRuntimeWebTargetPathState(params: {
|
||||
return fetch?.enabled !== false && fetch?.firecrawl?.enabled !== false ? "active" : "inactive";
|
||||
}
|
||||
|
||||
if (params.path === "tools.web.x_search.apiKey") {
|
||||
return params.config.tools?.web?.x_search?.enabled !== false ? "active" : "inactive";
|
||||
}
|
||||
|
||||
if (params.path === "tools.web.search.apiKey") {
|
||||
return params.config.tools?.web?.search?.enabled !== false ? "active" : "inactive";
|
||||
}
|
||||
@@ -144,6 +150,12 @@ function describeInactiveRuntimeWebTargetPath(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (params.path === "tools.web.x_search.apiKey") {
|
||||
return params.config.tools?.web?.x_search?.enabled === false
|
||||
? "tools.web.x_search is disabled."
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (params.path === "tools.web.search.apiKey") {
|
||||
return params.config.tools?.web?.search?.enabled === false
|
||||
? "tools.web.search is disabled."
|
||||
@@ -316,7 +328,9 @@ function isUnsupportedSecretsResolveError(err: unknown): boolean {
|
||||
|
||||
function isDirectRuntimeWebTargetPath(path: string): boolean {
|
||||
return (
|
||||
path === "tools.web.fetch.firecrawl.apiKey" || /^tools\.web\.search\.[^.]+\.apiKey$/.test(path)
|
||||
path === "tools.web.fetch.firecrawl.apiKey" ||
|
||||
path === "tools.web.x_search.apiKey" ||
|
||||
/^tools\.web\.search\.[^.]+\.apiKey$/.test(path)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ describe("command secret target ids", () => {
|
||||
expect(ids.has("agents.defaults.memorySearch.remote.apiKey")).toBe(true);
|
||||
expect(ids.has("agents.list[].memorySearch.remote.apiKey")).toBe(true);
|
||||
expect(ids.has("tools.web.fetch.firecrawl.apiKey")).toBe(true);
|
||||
expect(ids.has("tools.web.x_search.apiKey")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes gateway auth and channel targets for security audit", () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ const COMMAND_SECRET_TARGETS = {
|
||||
"messages.tts.",
|
||||
"tools.web.search",
|
||||
"tools.web.fetch.firecrawl.",
|
||||
"tools.web.x_search",
|
||||
]),
|
||||
status: idsByPrefix([
|
||||
"channels.",
|
||||
|
||||
@@ -5636,6 +5636,101 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
x_search: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
apiKey: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "env",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
pattern: "^[A-Z][A-Z0-9_]{0,127}$",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "file",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "exec",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
},
|
||||
inlineCitations: {
|
||||
type: "boolean",
|
||||
},
|
||||
maxTurns: {
|
||||
type: "integer",
|
||||
minimum: -9007199254740991,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
timeoutSeconds: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
cacheTtlMinutes: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -12844,6 +12939,42 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
||||
help: "Timeout in seconds for Firecrawl requests.",
|
||||
tags: ["performance", "tools"],
|
||||
},
|
||||
"tools.web.x_search.enabled": {
|
||||
label: "Enable X Search Tool",
|
||||
help: "Enable the x_search tool (requires XAI_API_KEY or tools.web.x_search.apiKey).",
|
||||
tags: ["tools"],
|
||||
},
|
||||
"tools.web.x_search.apiKey": {
|
||||
label: "xAI API Key",
|
||||
help: "xAI API key for X search (fallback: XAI_API_KEY env var).",
|
||||
tags: ["security", "auth", "tools"],
|
||||
sensitive: true,
|
||||
},
|
||||
"tools.web.x_search.model": {
|
||||
label: "X Search Model",
|
||||
help: 'Model to use for X search (default: "grok-4-1-fast-non-reasoning").',
|
||||
tags: ["models", "tools"],
|
||||
},
|
||||
"tools.web.x_search.inlineCitations": {
|
||||
label: "X Search Inline Citations",
|
||||
help: "Keep inline citations from xAI in x_search responses when available (default: false).",
|
||||
tags: ["tools"],
|
||||
},
|
||||
"tools.web.x_search.maxTurns": {
|
||||
label: "X Search Max Turns",
|
||||
help: "Optional max internal search/tool turns xAI may use per x_search request. Omit to let xAI choose.",
|
||||
tags: ["performance", "tools"],
|
||||
},
|
||||
"tools.web.x_search.timeoutSeconds": {
|
||||
label: "X Search Timeout (sec)",
|
||||
help: "Timeout in seconds for x_search requests.",
|
||||
tags: ["performance", "tools"],
|
||||
},
|
||||
"tools.web.x_search.cacheTtlMinutes": {
|
||||
label: "X Search Cache TTL (min)",
|
||||
help: "Cache TTL in minutes for x_search results.",
|
||||
tags: ["performance", "storage", "tools"],
|
||||
},
|
||||
"gateway.controlUi.basePath": {
|
||||
label: "Control UI Base Path",
|
||||
help: "Optional URL prefix where the Control UI is served (e.g. /openclaw).",
|
||||
|
||||
@@ -714,6 +714,16 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"tools.web.fetch.firecrawl.maxAgeMs":
|
||||
"Firecrawl maxAge (ms) for cached results when supported by the API.",
|
||||
"tools.web.fetch.firecrawl.timeoutSeconds": "Timeout in seconds for Firecrawl requests.",
|
||||
"tools.web.x_search.enabled":
|
||||
"Enable the x_search tool (requires XAI_API_KEY or tools.web.x_search.apiKey).",
|
||||
"tools.web.x_search.apiKey": "xAI API key for X search (fallback: XAI_API_KEY env var).",
|
||||
"tools.web.x_search.model": 'Model to use for X search (default: "grok-4-1-fast-non-reasoning").',
|
||||
"tools.web.x_search.inlineCitations":
|
||||
"Keep inline citations from xAI in x_search responses when available (default: false).",
|
||||
"tools.web.x_search.maxTurns":
|
||||
"Optional max internal search/tool turns xAI may use per x_search request. Omit to let xAI choose.",
|
||||
"tools.web.x_search.timeoutSeconds": "Timeout in seconds for x_search requests.",
|
||||
"tools.web.x_search.cacheTtlMinutes": "Cache TTL in minutes for x_search results.",
|
||||
models:
|
||||
"Model catalog root for provider definitions, merge/replace behavior, and optional Bedrock discovery integration. Keep provider definitions explicit and validated before relying on production failover paths.",
|
||||
"models.mode":
|
||||
|
||||
@@ -247,6 +247,13 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.web.fetch.firecrawl.onlyMainContent": "Firecrawl Main Content Only",
|
||||
"tools.web.fetch.firecrawl.maxAgeMs": "Firecrawl Cache Max Age (ms)",
|
||||
"tools.web.fetch.firecrawl.timeoutSeconds": "Firecrawl Timeout (sec)",
|
||||
"tools.web.x_search.enabled": "Enable X Search Tool",
|
||||
"tools.web.x_search.apiKey": "xAI API Key", // pragma: allowlist secret
|
||||
"tools.web.x_search.model": "X Search Model",
|
||||
"tools.web.x_search.inlineCitations": "X Search Inline Citations",
|
||||
"tools.web.x_search.maxTurns": "X Search Max Turns",
|
||||
"tools.web.x_search.timeoutSeconds": "X Search Timeout (sec)",
|
||||
"tools.web.x_search.cacheTtlMinutes": "X Search Cache TTL (min)",
|
||||
"gateway.controlUi.basePath": "Control UI Base Path",
|
||||
"gateway.controlUi.root": "Control UI Assets Root",
|
||||
"gateway.controlUi.allowedOrigins": "Control UI Allowed Origins",
|
||||
|
||||
@@ -459,6 +459,23 @@ type WebSearchLegacyProviderConfig = {
|
||||
inlineCitations?: boolean;
|
||||
};
|
||||
|
||||
type XSearchToolConfig = {
|
||||
/** Enable X search tool (default: true when an xAI API key is available). */
|
||||
enabled?: boolean;
|
||||
/** API key for xAI (defaults to XAI_API_KEY env var). Supports SecretRef. */
|
||||
apiKey?: SecretInput;
|
||||
/** Model id to use for X search. */
|
||||
model?: string;
|
||||
/** Keep inline citations in the xAI response payload when available. */
|
||||
inlineCitations?: boolean;
|
||||
/** Optional max search/tool turns for xAI to use internally. */
|
||||
maxTurns?: number;
|
||||
/** Timeout in seconds for X search requests. */
|
||||
timeoutSeconds?: number;
|
||||
/** Cache TTL in minutes for X search results. */
|
||||
cacheTtlMinutes?: number;
|
||||
};
|
||||
|
||||
export type ToolsConfig = {
|
||||
/** Base tool profile applied before allow/deny lists. */
|
||||
profile?: ToolProfileId;
|
||||
@@ -495,6 +512,8 @@ export type ToolsConfig = {
|
||||
/** @deprecated Legacy Perplexity scoped config. */
|
||||
perplexity?: WebSearchLegacyProviderConfig;
|
||||
} & Record<string, unknown>;
|
||||
/** X (formerly Twitter) search tool configuration using xAI Grok. */
|
||||
x_search?: XSearchToolConfig;
|
||||
fetch?: {
|
||||
/** Enable web fetch tool (default: true). */
|
||||
enabled?: boolean;
|
||||
|
||||
@@ -348,10 +348,24 @@ export const ToolsWebFetchSchema = z
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolsWebXSearchSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
apiKey: SecretInputSchema.optional().register(sensitive),
|
||||
model: z.string().optional(),
|
||||
inlineCitations: z.boolean().optional(),
|
||||
maxTurns: z.number().int().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
cacheTtlMinutes: z.number().nonnegative().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolsWebSchema = z
|
||||
.object({
|
||||
search: ToolsWebSearchSchema,
|
||||
fetch: ToolsWebFetchSchema,
|
||||
x_search: ToolsWebXSearchSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@@ -11,6 +11,8 @@ export type SecretResolverWarningCode =
|
||||
| "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT"
|
||||
| "WEB_SEARCH_KEY_UNRESOLVED_FALLBACK_USED"
|
||||
| "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK"
|
||||
| "WEB_X_SEARCH_KEY_UNRESOLVED_FALLBACK_USED"
|
||||
| "WEB_X_SEARCH_KEY_UNRESOLVED_NO_FALLBACK"
|
||||
| "WEB_FETCH_FIRECRAWL_KEY_UNRESOLVED_FALLBACK_USED"
|
||||
| "WEB_FETCH_FIRECRAWL_KEY_UNRESOLVED_NO_FALLBACK";
|
||||
|
||||
|
||||
@@ -798,4 +798,57 @@ describe("runtime web tools resolution", () => {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves x_search SecretRef and writes the resolved key into runtime config", async () => {
|
||||
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
X_SEARCH_REF: "x-search-runtime-key",
|
||||
},
|
||||
});
|
||||
|
||||
expect(metadata.xSearch.active).toBe(true);
|
||||
expect(metadata.xSearch.apiKeySource).toBe("secretRef");
|
||||
expect(resolvedConfig.tools?.web?.x_search?.apiKey).toBe("x-search-runtime-key");
|
||||
expect(context.warnings.map((warning) => warning.code)).not.toContain(
|
||||
"WEB_X_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses env fallback for unresolved x_search SecretRef when active", async () => {
|
||||
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: { source: "env", provider: "default", id: "MISSING_X_SEARCH_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
XAI_API_KEY: "x-search-fallback-key", // pragma: allowlist secret
|
||||
},
|
||||
});
|
||||
|
||||
expect(metadata.xSearch.active).toBe(true);
|
||||
expect(metadata.xSearch.apiKeySource).toBe("env");
|
||||
expect(resolvedConfig.tools?.web?.x_search?.apiKey).toBe("x-search-fallback-key");
|
||||
expect(context.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "WEB_X_SEARCH_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
path: "tools.web.x_search.apiKey",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
RuntimeWebFetchFirecrawlMetadata,
|
||||
RuntimeWebSearchMetadata,
|
||||
RuntimeWebToolsMetadata,
|
||||
RuntimeWebXSearchMetadata,
|
||||
} from "./runtime-web-tools.types.js";
|
||||
|
||||
type WebSearchProvider = string;
|
||||
@@ -34,6 +35,7 @@ export type {
|
||||
RuntimeWebFetchFirecrawlMetadata,
|
||||
RuntimeWebSearchMetadata,
|
||||
RuntimeWebToolsMetadata,
|
||||
RuntimeWebXSearchMetadata,
|
||||
};
|
||||
|
||||
type FetchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
@@ -262,6 +264,13 @@ function setResolvedFirecrawlApiKey(params: {
|
||||
firecrawl.apiKey = params.value;
|
||||
}
|
||||
|
||||
function setResolvedXSearchApiKey(params: { resolvedConfig: OpenClawConfig; value: string }): void {
|
||||
const tools = ensureObject(params.resolvedConfig as Record<string, unknown>, "tools");
|
||||
const web = ensureObject(tools, "web");
|
||||
const xSearch = ensureObject(web, "x_search");
|
||||
xSearch.apiKey = params.value;
|
||||
}
|
||||
|
||||
function keyPathForProvider(provider: PluginWebSearchProviderEntry): string {
|
||||
return provider.credentialPath;
|
||||
}
|
||||
@@ -574,6 +583,103 @@ export async function resolveRuntimeWebTools(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const xSearch = isRecord(web?.x_search) ? web.x_search : undefined;
|
||||
const xSearchEnabled = xSearch?.enabled !== false;
|
||||
const xSearchPath = "tools.web.x_search.apiKey";
|
||||
let xSearchResolution: SecretResolutionResult = {
|
||||
source: "missing",
|
||||
secretRefConfigured: false,
|
||||
fallbackUsedAfterRefFailure: false,
|
||||
};
|
||||
const xSearchDiagnostics: RuntimeWebDiagnostic[] = [];
|
||||
|
||||
if (xSearchEnabled) {
|
||||
xSearchResolution = await resolveSecretInputWithEnvFallback({
|
||||
sourceConfig: params.sourceConfig,
|
||||
context: params.context,
|
||||
defaults,
|
||||
value: xSearch?.apiKey,
|
||||
path: xSearchPath,
|
||||
envVars: ["XAI_API_KEY"],
|
||||
});
|
||||
|
||||
if (xSearchResolution.value) {
|
||||
setResolvedXSearchApiKey({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
value: xSearchResolution.value,
|
||||
});
|
||||
}
|
||||
|
||||
if (xSearchResolution.secretRefConfigured) {
|
||||
if (xSearchResolution.fallbackUsedAfterRefFailure) {
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_X_SEARCH_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
message:
|
||||
`${xSearchPath} SecretRef could not be resolved; using ${xSearchResolution.fallbackEnvVar ?? "env fallback"}. ` +
|
||||
(xSearchResolution.unresolvedRefReason ?? "").trim(),
|
||||
path: xSearchPath,
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
xSearchDiagnostics.push(diagnostic);
|
||||
pushWarning(params.context, {
|
||||
code: "WEB_X_SEARCH_KEY_UNRESOLVED_FALLBACK_USED",
|
||||
path: xSearchPath,
|
||||
message: diagnostic.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (!xSearchResolution.value && xSearchResolution.unresolvedRefReason) {
|
||||
const diagnostic: RuntimeWebDiagnostic = {
|
||||
code: "WEB_X_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
message: xSearchResolution.unresolvedRefReason,
|
||||
path: xSearchPath,
|
||||
};
|
||||
diagnostics.push(diagnostic);
|
||||
xSearchDiagnostics.push(diagnostic);
|
||||
pushWarning(params.context, {
|
||||
code: "WEB_X_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
|
||||
path: xSearchPath,
|
||||
message: xSearchResolution.unresolvedRefReason,
|
||||
});
|
||||
throw new Error(
|
||||
`[WEB_X_SEARCH_KEY_UNRESOLVED_NO_FALLBACK] ${xSearchResolution.unresolvedRefReason}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (hasConfiguredSecretRef(xSearch?.apiKey, defaults)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path: xSearchPath,
|
||||
details: "tools.web.x_search is disabled.",
|
||||
});
|
||||
xSearchResolution = {
|
||||
source: "secretRef",
|
||||
secretRefConfigured: true,
|
||||
fallbackUsedAfterRefFailure: false,
|
||||
};
|
||||
} else {
|
||||
const configuredInlineValue = normalizeSecretInput(xSearch?.apiKey);
|
||||
if (configuredInlineValue) {
|
||||
xSearchResolution = {
|
||||
value: configuredInlineValue,
|
||||
source: "config",
|
||||
secretRefConfigured: false,
|
||||
fallbackUsedAfterRefFailure: false,
|
||||
};
|
||||
} else {
|
||||
const envFallback = readNonEmptyEnvValue(params.context.env, ["XAI_API_KEY"]);
|
||||
if (envFallback.value) {
|
||||
xSearchResolution = {
|
||||
value: envFallback.value,
|
||||
source: "env",
|
||||
fallbackEnvVar: envFallback.envVar,
|
||||
secretRefConfigured: false,
|
||||
fallbackUsedAfterRefFailure: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = isRecord(web?.fetch) ? (web.fetch as FetchConfig) : undefined;
|
||||
const firecrawl = isRecord(fetch?.firecrawl) ? fetch.firecrawl : undefined;
|
||||
const fetchEnabled = fetch?.enabled !== false;
|
||||
@@ -681,6 +787,11 @@ export async function resolveRuntimeWebTools(params: {
|
||||
|
||||
return {
|
||||
search: searchMetadata,
|
||||
xSearch: {
|
||||
active: Boolean(xSearchEnabled && xSearchResolution.value),
|
||||
apiKeySource: xSearchResolution.source,
|
||||
diagnostics: xSearchDiagnostics,
|
||||
},
|
||||
fetch: {
|
||||
firecrawl: {
|
||||
active: firecrawlActive,
|
||||
|
||||
@@ -3,6 +3,8 @@ export type RuntimeWebDiagnosticCode =
|
||||
| "WEB_SEARCH_AUTODETECT_SELECTED"
|
||||
| "WEB_SEARCH_KEY_UNRESOLVED_FALLBACK_USED"
|
||||
| "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK"
|
||||
| "WEB_X_SEARCH_KEY_UNRESOLVED_FALLBACK_USED"
|
||||
| "WEB_X_SEARCH_KEY_UNRESOLVED_NO_FALLBACK"
|
||||
| "WEB_FETCH_FIRECRAWL_KEY_UNRESOLVED_FALLBACK_USED"
|
||||
| "WEB_FETCH_FIRECRAWL_KEY_UNRESOLVED_NO_FALLBACK";
|
||||
|
||||
@@ -27,8 +29,15 @@ export type RuntimeWebFetchFirecrawlMetadata = {
|
||||
diagnostics: RuntimeWebDiagnostic[];
|
||||
};
|
||||
|
||||
export type RuntimeWebXSearchMetadata = {
|
||||
active: boolean;
|
||||
apiKeySource: "config" | "secretRef" | "env" | "missing";
|
||||
diagnostics: RuntimeWebDiagnostic[];
|
||||
};
|
||||
|
||||
export type RuntimeWebToolsMetadata = {
|
||||
search: RuntimeWebSearchMetadata;
|
||||
xSearch: RuntimeWebXSearchMetadata;
|
||||
fetch: {
|
||||
firecrawl: RuntimeWebFetchFirecrawlMetadata;
|
||||
};
|
||||
|
||||
@@ -208,6 +208,9 @@ function buildConfigForOpenClawTarget(entry: SecretRegistryEntry, envId: string)
|
||||
if (entry.id === "plugins.entries.tavily.config.webSearch.apiKey") {
|
||||
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "tavily");
|
||||
}
|
||||
if (entry.id === "tools.web.x_search.apiKey") {
|
||||
setPathCreateStrict(config, ["tools", "web", "x_search", "enabled"], true);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -714,6 +714,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "tools.web.x_search.apiKey",
|
||||
targetType: "tools.web.x_search.apiKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "tools.web.x_search.apiKey",
|
||||
secretShape: SECRET_INPUT_SHAPE,
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "tools.web.search.apiKey",
|
||||
targetType: "tools.web.search.apiKey",
|
||||
|
||||
@@ -274,6 +274,11 @@ describe("web search runtime", () => {
|
||||
selectedProvider: "beta",
|
||||
diagnostics: [],
|
||||
},
|
||||
xSearch: {
|
||||
active: false,
|
||||
apiKeySource: "missing",
|
||||
diagnostics: [],
|
||||
},
|
||||
fetch: {
|
||||
firecrawl: {
|
||||
active: false,
|
||||
|
||||
Reference in New Issue
Block a user