mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 23:56:07 +00:00
fix(xai): make x_search auth plugin-owned (#59691)
* fix(xai): make x_search auth plugin-owned * fix(xai): restore x_search runtime migration fallback * fix(xai): narrow legacy x_search auth migration * fix(secrets): drop legacy x_search target registry entry * fix(xai): no-op knob-only x_search migration fallback
This commit is contained in:
@@ -67338,55 +67338,6 @@
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "tools.web.x_search.apiKey",
|
||||
"kind": "core",
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": true,
|
||||
"tags": [
|
||||
"auth",
|
||||
"security",
|
||||
"tools"
|
||||
],
|
||||
"label": "xAI API Key",
|
||||
"help": "xAI API key for X search (fallback: XAI_API_KEY env var).",
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "tools.web.x_search.apiKey.id",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.x_search.apiKey.provider",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.x_search.apiKey.source",
|
||||
"kind": "core",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "tools.web.x_search.cacheTtlMinutes",
|
||||
"kind": "core",
|
||||
@@ -67394,13 +67345,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"performance",
|
||||
"storage",
|
||||
"tools"
|
||||
],
|
||||
"label": "X Search Cache TTL (min)",
|
||||
"help": "Cache TTL in minutes for x_search results.",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -67410,11 +67355,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"tools"
|
||||
],
|
||||
"label": "Enable X Search Tool",
|
||||
"help": "Enable the x_search tool (requires XAI_API_KEY or tools.web.x_search.apiKey).",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -67424,11 +67365,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"tools"
|
||||
],
|
||||
"label": "X Search Inline Citations",
|
||||
"help": "Keep inline citations from xAI in x_search responses when available (default: false).",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -67438,12 +67375,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"performance",
|
||||
"tools"
|
||||
],
|
||||
"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": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -67453,12 +67385,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"models",
|
||||
"tools"
|
||||
],
|
||||
"label": "X Search Model",
|
||||
"help": "Model to use for X search (default: \"grok-4-1-fast-non-reasoning\").",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
@@ -67468,12 +67395,7 @@
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [
|
||||
"performance",
|
||||
"tools"
|
||||
],
|
||||
"label": "X Search Timeout (sec)",
|
||||
"help": "Timeout in seconds for x_search requests.",
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5781}
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5777}
|
||||
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -5742,16 +5742,12 @@
|
||||
{"recordType":"path","path":"tools.web.search.provider","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Web Search Provider","help":"Search provider id. Auto-detected from available API keys if omitted.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.search.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"Web Search Timeout (sec)","help":"Timeout in seconds for web_search requests.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.x_search.apiKey","kind":"core","type":["object","string"],"required":false,"deprecated":false,"sensitive":true,"tags":["auth","security","tools"],"label":"xAI API Key","help":"xAI API key for X search (fallback: XAI_API_KEY env var).","hasChildren":true}
|
||||
{"recordType":"path","path":"tools.web.x_search.apiKey.id","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.apiKey.provider","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.apiKey.source","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.cacheTtlMinutes","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":["performance","storage","tools"],"label":"X Search Cache TTL (min)","help":"Cache TTL in minutes for x_search results.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"Enable X Search Tool","help":"Enable the x_search tool (requires XAI_API_KEY or tools.web.x_search.apiKey).","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.inlineCitations","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["tools"],"label":"X Search Inline Citations","help":"Keep inline citations from xAI in x_search responses when available (default: false).","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.maxTurns","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"X Search Max Turns","help":"Optional max internal search/tool turns xAI may use per x_search request. Omit to let xAI choose.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["models","tools"],"label":"X Search Model","help":"Model to use for X search (default: \"grok-4-1-fast-non-reasoning\").","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance","tools"],"label":"X Search Timeout (sec)","help":"Timeout in seconds for x_search requests.","hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.cacheTtlMinutes","kind":"core","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.inlineCitations","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.maxTurns","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.model","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"tools.web.x_search.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"ui","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"UI","help":"UI presentation settings for accenting and assistant identity shown in control surfaces. Use this for branding and readability customization without changing runtime behavior.","hasChildren":true}
|
||||
{"recordType":"path","path":"ui.assistant","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Assistant Appearance","help":"Assistant display identity settings for name and avatar shown in UI surfaces. Keep these values aligned with your operator-facing persona and support expectations.","hasChildren":true}
|
||||
{"recordType":"path","path":"ui.assistant.avatar","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Assistant Avatar","help":"Assistant avatar image source used in UI surfaces (URL, path, or data URI depending on runtime support). Use trusted assets and consistent branding dimensions for clean rendering.","hasChildren":false}
|
||||
|
||||
@@ -39,7 +39,6 @@ Scope intent:
|
||||
- `plugins.entries.firecrawl.config.webSearch.apiKey`
|
||||
- `plugins.entries.tavily.config.webSearch.apiKey`
|
||||
- `tools.web.search.apiKey`
|
||||
- `tools.web.x_search.apiKey`
|
||||
- `gateway.auth.password`
|
||||
- `gateway.auth.token`
|
||||
- `gateway.remote.token`
|
||||
|
||||
@@ -523,13 +523,6 @@
|
||||
"path": "tools.web.search.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "tools.web.x_search.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "tools.web.x_search.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -231,6 +231,44 @@ describe("xai x_search tool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses migrated runtime auth when the source config still carries legacy x_search apiKey", async () => {
|
||||
const mockFetch = installXSearchFetch();
|
||||
const tool = createXSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: "legacy-x-search-key", // pragma: allowlist secret
|
||||
enabled: true,
|
||||
} as Record<string, unknown>,
|
||||
},
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "migrated-runtime-key", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tool?.execute?.("x-search:migrated-runtime-key", {
|
||||
query: "migrated runtime auth",
|
||||
});
|
||||
|
||||
const request = mockFetch.mock.calls[0]?.[1] as RequestInit | undefined;
|
||||
expect((request?.headers as Record<string, string> | undefined)?.Authorization).toBe(
|
||||
"Bearer migrated-runtime-key",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid date ordering before calling xAI", async () => {
|
||||
const mockFetch = installXSearchFetch();
|
||||
const tool = createXSearchTool({
|
||||
|
||||
@@ -77,11 +77,6 @@ function resolveFallbackXaiApiKey(cfg?: OpenClawConfig): string | undefined {
|
||||
return readPluginXaiWebSearchApiKey(cfg) ?? readLegacyGrokApiKey(cfg);
|
||||
}
|
||||
|
||||
function readLegacyXSearchApiKey(cfg?: OpenClawConfig): string | undefined {
|
||||
const legacyConfig = resolveLegacyXSearchConfig(cfg);
|
||||
return readConfiguredSecretString(legacyConfig?.apiKey, "tools.web.x_search.apiKey");
|
||||
}
|
||||
|
||||
function resolveXSearchConfig(cfg?: OpenClawConfig): Record<string, unknown> | undefined {
|
||||
return resolveEffectiveXSearchConfig(cfg);
|
||||
}
|
||||
@@ -94,33 +89,19 @@ function resolveXSearchEnabled(params: {
|
||||
if (params.config?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
resolveFallbackXaiApiKey(params.runtimeConfig) ||
|
||||
readLegacyXSearchApiKey(params.runtimeConfig)
|
||||
) {
|
||||
if (resolveFallbackXaiApiKey(params.runtimeConfig)) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(
|
||||
resolveFallbackXaiApiKey(params.cfg) ||
|
||||
readLegacyXSearchApiKey(params.cfg) ||
|
||||
readProviderEnvValue(["XAI_API_KEY"]),
|
||||
);
|
||||
return Boolean(resolveFallbackXaiApiKey(params.cfg) || readProviderEnvValue(["XAI_API_KEY"]));
|
||||
}
|
||||
|
||||
function resolveXSearchApiKey(params: {
|
||||
sourceConfig?: OpenClawConfig;
|
||||
runtimeConfig?: OpenClawConfig;
|
||||
}): string | undefined {
|
||||
const sourceXSearchConfig = resolveXSearchConfig(params.sourceConfig);
|
||||
const runtimeXSearchConfig =
|
||||
params.runtimeConfig && params.runtimeConfig !== params.sourceConfig
|
||||
? resolveXSearchConfig(params.runtimeConfig)
|
||||
: undefined;
|
||||
return (
|
||||
resolveFallbackXaiApiKey(params.runtimeConfig) ??
|
||||
resolveFallbackXaiApiKey(params.sourceConfig) ??
|
||||
readLegacyXSearchApiKey(params.runtimeConfig) ??
|
||||
readLegacyXSearchApiKey(params.sourceConfig) ??
|
||||
readProviderEnvValue(["XAI_API_KEY"])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ describe("normalizeCompatibilityConfigValues", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("migrates legacy x_search config into xai plugin-owned config", () => {
|
||||
it("migrates legacy x_search auth into xai plugin-owned config", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
tools: {
|
||||
web: {
|
||||
@@ -138,25 +138,21 @@ describe("normalizeCompatibilityConfigValues", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
(res.config.tools?.web as Record<string, unknown> | undefined)?.x_search,
|
||||
).toBeUndefined();
|
||||
expect((res.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
expect(res.config.plugins?.entries?.xai).toEqual({
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-legacy-key",
|
||||
},
|
||||
xSearch: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.changes).toEqual(
|
||||
expect.arrayContaining([
|
||||
"Moved tools.web.x_search.apiKey → plugins.entries.xai.config.webSearch.apiKey.",
|
||||
"Moved tools.web.x_search → plugins.entries.xai.config.xSearch.",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -286,6 +286,38 @@ describe("legacy migrate tts provider shape", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy migrate x_search auth", () => {
|
||||
it("moves only legacy x_search auth into plugin-owned xai config", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: "xai-legacy-key",
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect((res.config?.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
expect(res.config?.plugins?.entries?.xai).toEqual({
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-legacy-key",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.changes).toEqual([
|
||||
"Moved tools.web.x_search.apiKey → plugins.entries.xai.config.webSearch.apiKey.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy migrate heartbeat config", () => {
|
||||
it("moves top-level heartbeat into agents.defaults.heartbeat", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { OpenClawConfig } from "./config.js";
|
||||
import { listLegacyXSearchConfigPaths, migrateLegacyXSearchConfig } from "./legacy-x-search.js";
|
||||
|
||||
describe("legacy x_search config migration", () => {
|
||||
it("moves legacy x_search auth and settings into the xai plugin config", () => {
|
||||
it("moves only legacy x_search auth into the xai plugin config", () => {
|
||||
const res = migrateLegacyXSearchConfig({
|
||||
tools: {
|
||||
web: {
|
||||
@@ -16,28 +16,24 @@ describe("legacy x_search config migration", () => {
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
|
||||
expect(
|
||||
(res.config.tools?.web as Record<string, unknown> | undefined)?.x_search,
|
||||
).toBeUndefined();
|
||||
expect((res.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
expect(res.config.plugins?.entries?.xai).toEqual({
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-legacy-key",
|
||||
},
|
||||
xSearch: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.changes).toEqual([
|
||||
"Moved tools.web.x_search.apiKey → plugins.entries.xai.config.webSearch.apiKey.",
|
||||
"Moved tools.web.x_search → plugins.entries.xai.config.xSearch.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps explicit plugin-owned values when migrating legacy x_search config", () => {
|
||||
it("keeps explicit plugin-owned auth when migrating legacy x_search config", () => {
|
||||
const res = migrateLegacyXSearchConfig({
|
||||
tools: {
|
||||
web: {
|
||||
@@ -66,18 +62,40 @@ describe("legacy x_search config migration", () => {
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
|
||||
expect((res.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
model: "legacy-model",
|
||||
cacheTtlMinutes: 5,
|
||||
});
|
||||
expect(res.config.plugins?.entries?.xai?.config).toEqual({
|
||||
webSearch: {
|
||||
apiKey: "plugin-key",
|
||||
},
|
||||
xSearch: {
|
||||
enabled: true,
|
||||
model: "plugin-model",
|
||||
cacheTtlMinutes: 5,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does nothing for knob-only x_search config without a legacy apiKey", () => {
|
||||
const config = {
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
} as Record<string, unknown>,
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const res = migrateLegacyXSearchConfig(config);
|
||||
|
||||
expect(res.config).toEqual(config);
|
||||
expect(res.changes).toEqual([]);
|
||||
expect(res.config.plugins?.entries?.xai).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lists legacy x_search paths", () => {
|
||||
expect(
|
||||
listLegacyXSearchConfigPaths({
|
||||
@@ -90,6 +108,6 @@ describe("legacy x_search config migration", () => {
|
||||
} as Record<string, unknown>,
|
||||
},
|
||||
} as OpenClawConfig),
|
||||
).toEqual(["tools.web.x_search.apiKey", "tools.web.x_search.enabled"]);
|
||||
).toEqual(["tools.web.x_search.apiKey"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { mergeMissing } from "./legacy.shared.js";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
const XAI_PLUGIN_ID = "xai";
|
||||
const X_SEARCH_LEGACY_PATH = "tools.web.x_search";
|
||||
const X_SEARCH_PLUGIN_PATH = `plugins.entries.${XAI_PLUGIN_ID}.config.xSearch`;
|
||||
const XAI_WEB_SEARCH_PLUGIN_KEY_PATH = `plugins.entries.${XAI_PLUGIN_ID}.config.webSearch.apiKey`;
|
||||
|
||||
function isRecord(value: unknown): value is JsonRecord {
|
||||
@@ -37,29 +34,16 @@ function resolveLegacyXSearchConfig(raw: unknown): JsonRecord | undefined {
|
||||
return isRecord(web?.x_search) ? web.x_search : undefined;
|
||||
}
|
||||
|
||||
function splitLegacyXSearchPayload(legacy: JsonRecord): {
|
||||
auth: unknown;
|
||||
xSearch: JsonRecord | undefined;
|
||||
} {
|
||||
const next: JsonRecord = {};
|
||||
for (const [key, value] of Object.entries(legacy)) {
|
||||
if (key === "apiKey") {
|
||||
continue;
|
||||
}
|
||||
next[key] = value;
|
||||
}
|
||||
return {
|
||||
auth: legacy.apiKey,
|
||||
xSearch: Object.keys(next).length > 0 ? next : undefined,
|
||||
};
|
||||
function resolveLegacyXSearchAuth(legacy: JsonRecord): unknown {
|
||||
return legacy.apiKey;
|
||||
}
|
||||
|
||||
export function listLegacyXSearchConfigPaths(raw: unknown): string[] {
|
||||
const legacy = resolveLegacyXSearchConfig(raw);
|
||||
if (!legacy) {
|
||||
if (!legacy || !Object.prototype.hasOwnProperty.call(legacy, "apiKey")) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(legacy).map((key) => `${X_SEARCH_LEGACY_PATH}.${key}`);
|
||||
return [`${X_SEARCH_LEGACY_PATH}.apiKey`];
|
||||
}
|
||||
|
||||
export function migrateLegacyXSearchConfig<T>(raw: T): { config: T; changes: string[] } {
|
||||
@@ -67,14 +51,20 @@ export function migrateLegacyXSearchConfig<T>(raw: T): { config: T; changes: str
|
||||
return { config: raw, changes: [] };
|
||||
}
|
||||
const legacy = resolveLegacyXSearchConfig(raw);
|
||||
if (!legacy) {
|
||||
if (!legacy || !Object.prototype.hasOwnProperty.call(legacy, "apiKey")) {
|
||||
return { config: raw, changes: [] };
|
||||
}
|
||||
|
||||
const nextRoot = structuredClone(raw);
|
||||
const tools = ensureRecord(nextRoot, "tools");
|
||||
const web = ensureRecord(tools, "web");
|
||||
delete web.x_search;
|
||||
const nextLegacy = cloneRecord(legacy) ?? {};
|
||||
delete nextLegacy.apiKey;
|
||||
if (Object.keys(nextLegacy).length === 0) {
|
||||
delete web.x_search;
|
||||
} else {
|
||||
web.x_search = nextLegacy;
|
||||
}
|
||||
|
||||
const plugins = ensureRecord(nextRoot, "plugins");
|
||||
const entries = ensureRecord(plugins, "entries");
|
||||
@@ -84,7 +74,7 @@ export function migrateLegacyXSearchConfig<T>(raw: T): { config: T; changes: str
|
||||
entry.enabled = true;
|
||||
}
|
||||
const config = ensureRecord(entry, "config");
|
||||
const { auth, xSearch } = splitLegacyXSearchPayload(legacy);
|
||||
const auth = resolveLegacyXSearchAuth(legacy);
|
||||
const changes: string[] = [];
|
||||
|
||||
if (auth !== undefined) {
|
||||
@@ -107,24 +97,7 @@ export function migrateLegacyXSearchConfig<T>(raw: T): { config: T; changes: str
|
||||
}
|
||||
}
|
||||
|
||||
if (xSearch) {
|
||||
const existingXSearch = isRecord(config.xSearch) ? cloneRecord(config.xSearch) : undefined;
|
||||
if (!existingXSearch) {
|
||||
config.xSearch = cloneRecord(xSearch);
|
||||
changes.push(`Moved ${X_SEARCH_LEGACY_PATH} → ${X_SEARCH_PLUGIN_PATH}.`);
|
||||
} else {
|
||||
const merged = cloneRecord(existingXSearch);
|
||||
mergeMissing(merged, xSearch);
|
||||
config.xSearch = merged;
|
||||
if (JSON.stringify(existingXSearch) !== JSON.stringify(merged) || !hadEnabled) {
|
||||
changes.push(
|
||||
`Merged ${X_SEARCH_LEGACY_PATH} → ${X_SEARCH_PLUGIN_PATH} (filled missing fields from legacy; kept explicit plugin config values).`,
|
||||
);
|
||||
} else {
|
||||
changes.push(`Removed ${X_SEARCH_LEGACY_PATH} (${X_SEARCH_PLUGIN_PATH} already set).`);
|
||||
}
|
||||
}
|
||||
} else if (!hadEnabled) {
|
||||
if (auth !== undefined && Object.keys(nextLegacy).length === 0 && !hadEnabled) {
|
||||
changes.push(`Removed empty ${X_SEARCH_LEGACY_PATH}.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isGatewayNonLoopbackBindMode,
|
||||
resolveGatewayPortWithDefault,
|
||||
} from "./gateway-control-ui-origins.js";
|
||||
import { migrateLegacyXSearchConfig } from "./legacy-x-search.js";
|
||||
import {
|
||||
defineLegacyConfigMigration,
|
||||
ensureRecord,
|
||||
@@ -259,6 +260,12 @@ const HEARTBEAT_RULE: LegacyConfigRule = {
|
||||
"top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).",
|
||||
};
|
||||
|
||||
const X_SEARCH_RULE: LegacyConfigRule = {
|
||||
path: ["tools", "web", "x_search", "apiKey"],
|
||||
message:
|
||||
"tools.web.x_search.apiKey moved to the xAI plugin; use plugins.entries.xai.config.webSearch.apiKey instead (auto-migrated on load).",
|
||||
};
|
||||
|
||||
const LEGACY_TTS_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["messages", "tts"],
|
||||
@@ -287,6 +294,22 @@ const LEGACY_TTS_RULES: LegacyConfigRule[] = [
|
||||
];
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_RUNTIME: LegacyConfigMigrationSpec[] = [
|
||||
defineLegacyConfigMigration({
|
||||
id: "tools.web.x_search.apiKey->plugins.entries.xai.config.webSearch.apiKey",
|
||||
describe: "Move legacy x_search auth into the xAI plugin webSearch config",
|
||||
legacyRules: [X_SEARCH_RULE],
|
||||
apply: (raw, changes) => {
|
||||
const migrated = migrateLegacyXSearchConfig(raw);
|
||||
if (!migrated.changes.length) {
|
||||
return;
|
||||
}
|
||||
for (const key of Object.keys(raw)) {
|
||||
delete raw[key];
|
||||
}
|
||||
Object.assign(raw, migrated.config);
|
||||
changes.push(...migrated.changes);
|
||||
},
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
// v2026.2.26 added a startup guard requiring gateway.controlUi.allowedOrigins (or the
|
||||
// host-header fallback flag) for any non-loopback bind. The setup wizard was updated
|
||||
|
||||
@@ -5342,72 +5342,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
||||
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",
|
||||
},
|
||||
@@ -15221,10 +15155,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "tools"],
|
||||
},
|
||||
"tools.web.x_search.apiKey": {
|
||||
sensitive: true,
|
||||
tags: ["security", "auth", "tools"],
|
||||
},
|
||||
"mcp.servers.*.headers.*": {
|
||||
sensitive: true,
|
||||
tags: ["security"],
|
||||
|
||||
@@ -503,6 +503,21 @@ export type ToolsConfig = {
|
||||
};
|
||||
};
|
||||
} & Record<string, unknown>;
|
||||
/** X (formerly Twitter) search tool configuration using xAI Grok. */
|
||||
x_search?: {
|
||||
/** Enable X search tool (default: true when xAI auth is available via plugin config or XAI_API_KEY). */
|
||||
enabled?: boolean;
|
||||
/** 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;
|
||||
};
|
||||
fetch?: {
|
||||
/** Enable web fetch tool (default: true). */
|
||||
enabled?: boolean;
|
||||
|
||||
@@ -345,7 +345,6 @@ export const ToolsWebFetchSchema = z
|
||||
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(),
|
||||
|
||||
@@ -2739,17 +2739,73 @@ describe("secrets runtime snapshot", () => {
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(
|
||||
(snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search,
|
||||
).toBeUndefined();
|
||||
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
expect(snapshot.config.plugins?.entries?.xai?.config).toEqual({
|
||||
webSearch: {
|
||||
apiKey: "xai-runtime-key",
|
||||
},
|
||||
xSearch: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
});
|
||||
|
||||
it("still migrates legacy x_search auth when general legacy migration returns an invalid config", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" },
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
groupMentionsOnly: true,
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
X_SEARCH_KEY_REF: "xai-runtime-key-invalid-config",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(snapshot.config.plugins?.entries?.xai?.config).toEqual({
|
||||
webSearch: {
|
||||
apiKey: "xai-runtime-key-invalid-config",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not force-enable xai at runtime for knob-only x_search config", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
expect(snapshot.config.plugins?.entries?.xai).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,10 +169,10 @@ export async function prepareSecretsRuntimeSnapshot(params: {
|
||||
loadablePluginOrigins?: ReadonlyMap<string, PluginOrigin>;
|
||||
}): Promise<PreparedSecretsRuntimeSnapshot> {
|
||||
const runtimeEnv = mergeSecretsRuntimeEnv(params.env);
|
||||
const sourceConfig = structuredClone(migrateLegacyXSearchConfig(params.config).config);
|
||||
const resolvedConfig = structuredClone(
|
||||
migrateLegacyXSearchConfig(migrateLegacyConfig(params.config).config ?? params.config).config,
|
||||
);
|
||||
const migrated = migrateLegacyConfig(params.config);
|
||||
const migratedConfig = migrated.config ?? migrateLegacyXSearchConfig(params.config).config;
|
||||
const sourceConfig = structuredClone(migratedConfig);
|
||||
const resolvedConfig = structuredClone(migratedConfig);
|
||||
const loadablePluginOrigins =
|
||||
params.loadablePluginOrigins ??
|
||||
resolveLoadablePluginOrigins({ config: sourceConfig, env: runtimeEnv });
|
||||
|
||||
@@ -714,17 +714,6 @@ 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: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
targetType: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
|
||||
Reference in New Issue
Block a user