Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
// Xai helper module supports code execution config behavior.
import { isXaiToolEnabled, type XaiToolAuthContext } from "./tool-auth-shared.js";
export type CodeExecutionConfig = {
enabled?: boolean;
model?: string;
maxTurns?: number;
timeoutSeconds?: number;
};
export function readCodeExecutionConfigRecord(
config?: CodeExecutionConfig,
): Record<string, unknown> | undefined {
return config && typeof config === "object" ? (config as Record<string, unknown>) : undefined;
}
export function readPluginCodeExecutionConfig(cfg?: unknown): CodeExecutionConfig | undefined {
if (!cfg || typeof cfg !== "object") {
return undefined;
}
const entries = (cfg as Record<string, unknown>).plugins;
const pluginEntries =
entries && typeof entries === "object"
? ((entries as Record<string, unknown>).entries as Record<string, unknown> | undefined)
: undefined;
if (!pluginEntries) {
return undefined;
}
const xaiEntry = pluginEntries.xai;
if (!xaiEntry || typeof xaiEntry !== "object") {
return undefined;
}
const config = (xaiEntry as Record<string, unknown>).config;
if (!config || typeof config !== "object") {
return undefined;
}
const codeExecution = (config as Record<string, unknown>).codeExecution;
if (!codeExecution || typeof codeExecution !== "object") {
return undefined;
}
return codeExecution as CodeExecutionConfig;
}
export function resolveCodeExecutionEnabled(params: {
sourceConfig?: unknown;
runtimeConfig?: unknown;
config?: CodeExecutionConfig;
auth?: XaiToolAuthContext;
}): boolean {
return isXaiToolEnabled({
enabled: readCodeExecutionConfigRecord(params.config)?.enabled as boolean | undefined,
runtimeConfig: params.runtimeConfig as never,
sourceConfig: params.sourceConfig as never,
auth: params.auth,
});
}

View File

@@ -0,0 +1,111 @@
// Xai plugin module implements code execution shared behavior.
import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http";
import { postTrustedWebToolsJson } from "openclaw/plugin-sdk/provider-web-search";
import {
buildXaiResponsesToolBody,
requireXaiResponseTextAndCitations,
XAI_RESPONSES_ENDPOINT,
} from "./responses-tool-shared.js";
import {
resolveNormalizedXaiToolModel,
resolvePositiveIntegerToolConfig,
} from "./tool-config-shared.js";
import type { XaiWebSearchResponse } from "./web-search-shared.js";
const XAI_CODE_EXECUTION_ENDPOINT = XAI_RESPONSES_ENDPOINT;
const XAI_DEFAULT_CODE_EXECUTION_MODEL = "grok-4-1-fast";
type XaiCodeExecutionResponse = XaiWebSearchResponse & {
output?: Array<{
type?: string;
}>;
};
type XaiCodeExecutionResult = {
content: string;
citations: string[];
usedCodeExecution: boolean;
outputTypes: string[];
};
export function resolveXaiCodeExecutionModel(config?: Record<string, unknown>): string {
return resolveNormalizedXaiToolModel({
config,
defaultModel: XAI_DEFAULT_CODE_EXECUTION_MODEL,
});
}
export function resolveXaiCodeExecutionMaxTurns(
config?: Record<string, unknown>,
): number | undefined {
return resolvePositiveIntegerToolConfig(config, "maxTurns");
}
export function buildXaiCodeExecutionPayload(params: {
task: string;
model: string;
tookMs: number;
content: string;
citations: string[];
usedCodeExecution: boolean;
outputTypes: string[];
}): Record<string, unknown> {
return {
task: params.task,
provider: "xai",
model: params.model,
tookMs: params.tookMs,
content: params.content,
citations: params.citations,
usedCodeExecution: params.usedCodeExecution,
outputTypes: params.outputTypes,
};
}
export async function requestXaiCodeExecution(params: {
apiKey: string;
model: string;
timeoutSeconds: number;
maxTurns?: number;
task: string;
}): Promise<XaiCodeExecutionResult> {
return await postTrustedWebToolsJson(
{
url: XAI_CODE_EXECUTION_ENDPOINT,
timeoutSeconds: params.timeoutSeconds,
apiKey: params.apiKey,
body: buildXaiResponsesToolBody({
model: params.model,
inputText: params.task,
tools: [{ type: "code_interpreter" }],
maxTurns: params.maxTurns,
}),
errorLabel: "xAI",
},
async (response) => {
const data = (await readProviderJsonObjectResponse(
response,
"xAI code execution failed",
)) as XaiCodeExecutionResponse;
const { content, citations } = requireXaiResponseTextAndCitations(
data,
"xAI code execution failed",
);
const outputTypes = Array.isArray(data.output)
? [
...new Set(
data.output
.map((entry) => entry?.type)
.filter((value): value is string => Boolean(value)),
),
]
: [];
return {
content,
citations,
usedCodeExecution: outputTypes.includes("code_interpreter_call"),
outputTypes,
};
},
);
}

View File

@@ -0,0 +1,108 @@
// Xai tests cover responses tool shared plugin behavior.
import { describe, expect, it } from "vitest";
import { testing } from "./responses-tool-shared.js";
describe("xai responses tool helpers", () => {
it("builds the shared xAI Responses tool body", () => {
expect(
testing.buildXaiResponsesToolBody({
model: "grok-4-1-fast",
inputText: "search for openclaw",
tools: [{ type: "x_search" }],
maxTurns: 2,
}),
).toEqual({
model: "grok-4-1-fast",
input: [{ role: "user", content: "search for openclaw" }],
tools: [{ type: "x_search" }],
max_turns: 2,
});
});
it("falls back to annotation citations when the API omits top-level citations", () => {
expect(
testing.resolveXaiResponseTextAndCitations({
output: [
{
type: "message",
content: [
{
type: "output_text",
text: "Found it",
annotations: [{ type: "url_citation", url: "https://example.com/a" }],
},
],
},
],
}),
).toEqual({
content: "Found it",
citations: ["https://example.com/a"],
});
});
it("ignores malformed output, content, and annotation entries", () => {
expect(
testing.extractXaiWebSearchContent({
output: [
null,
{
type: "message",
content: [
null,
{
type: "output_text",
text: "Found it",
annotations: [
null,
{ type: "url_citation", url: "https://example.com/a" },
{ type: "url_citation", url: "https://example.com/a" },
{ type: "url_citation" },
],
},
],
},
],
}),
).toEqual({
text: "Found it",
annotationCitations: ["https://example.com/a"],
});
});
it("prefers explicit top-level citations when present", () => {
expect(
testing.resolveXaiResponseTextAndCitations({
output_text: "Done",
citations: ["https://example.com/b"],
}),
).toEqual({
content: "Done",
citations: ["https://example.com/b"],
});
});
it("includes inline citations only when enabled", () => {
const data = {
output_text: "Done",
citations: ["https://example.com/b"],
inline_citations: [{ start_index: 0, end_index: 4, url: "https://example.com/b" }],
};
expect(testing.resolveXaiResponseTextCitationsAndInline(data, true)).toEqual({
content: "Done",
citations: ["https://example.com/b"],
inlineCitations: [{ start_index: 0, end_index: 4, url: "https://example.com/b" }],
});
expect(testing.resolveXaiResponseTextCitationsAndInline(data, false)).toEqual({
content: "Done",
citations: ["https://example.com/b"],
inlineCitations: undefined,
});
});
it("rejects successful Responses tool payloads without answer text", () => {
expect(() => testing.requireXaiResponseTextAndCitations({}, "xAI tool failed")).toThrow(
"xAI tool failed: malformed JSON response",
);
});
});

View File

@@ -0,0 +1,164 @@
// Xai plugin module implements responses tool shared behavior.
import {
normalizeOptionalString as trimString,
uniqueStrings,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { XaiWebSearchResponse } from "./web-search-response.types.js";
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object";
}
function extractUrlCitations(annotations: unknown): string[] {
if (!Array.isArray(annotations)) {
return [];
}
return annotations
.filter(
(annotation) =>
isRecord(annotation) &&
annotation.type === "url_citation" &&
typeof annotation.url === "string",
)
.map((annotation) => annotation.url as string);
}
const XAI_RESPONSES_BASE_URL = "https://api.x.ai/v1";
export const XAI_RESPONSES_ENDPOINT = `${XAI_RESPONSES_BASE_URL}/responses`;
export function resolveXaiResponsesEndpoint(baseUrl?: unknown): string {
return `${(trimString(baseUrl) ?? XAI_RESPONSES_BASE_URL).replace(/\/+$/, "")}/responses`;
}
export function buildXaiResponsesToolBody(params: {
model: string;
inputText: string;
tools: Array<Record<string, unknown>>;
maxTurns?: number;
}): Record<string, unknown> {
return {
model: params.model,
input: [{ role: "user", content: params.inputText }],
tools: params.tools,
...(params.maxTurns ? { max_turns: params.maxTurns } : {}),
};
}
export function extractXaiWebSearchContent(data: XaiWebSearchResponse): {
text: string | undefined;
annotationCitations: string[];
} {
for (const output of data.output ?? []) {
if (!isRecord(output)) {
continue;
}
if (output.type === "message") {
const content = Array.isArray(output.content) ? output.content : [];
for (const block of content) {
if (!isRecord(block)) {
continue;
}
if (block.type === "output_text" && typeof block.text === "string" && block.text) {
const urls = extractUrlCitations(block.annotations);
return { text: block.text, annotationCitations: uniqueStrings(urls) };
}
}
}
if (output.type === "output_text" && typeof output.text === "string" && output.text) {
const urls = extractUrlCitations(output.annotations);
return { text: output.text, annotationCitations: uniqueStrings(urls) };
}
}
return {
text: typeof data.output_text === "string" ? data.output_text : undefined,
annotationCitations: [],
};
}
export function resolveXaiResponseTextAndCitations(data: XaiWebSearchResponse): {
content: string;
citations: string[];
} {
const { text, annotationCitations } = extractXaiWebSearchContent(data);
return {
content: text ?? "No response",
citations:
Array.isArray(data.citations) && data.citations.length > 0
? data.citations
: annotationCitations,
};
}
export function requireXaiResponseTextAndCitations(
data: XaiWebSearchResponse,
label: string,
): {
content: string;
citations: string[];
} {
const { text, annotationCitations } = extractXaiWebSearchContent(data);
if (!text) {
throw new Error(`${label}: malformed JSON response`);
}
return {
content: text,
citations:
Array.isArray(data.citations) && data.citations.length > 0
? data.citations
: annotationCitations,
};
}
export function resolveXaiResponseTextCitationsAndInline(
data: XaiWebSearchResponse,
inlineCitationsEnabled: boolean,
): {
content: string;
citations: string[];
inlineCitations?: XaiWebSearchResponse["inline_citations"];
} {
const { content, citations } = resolveXaiResponseTextAndCitations(data);
return {
content,
citations,
inlineCitations:
inlineCitationsEnabled && Array.isArray(data.inline_citations)
? data.inline_citations
: undefined,
};
}
export function requireXaiResponseTextCitationsAndInline(
data: XaiWebSearchResponse,
label: string,
inlineCitationsEnabled: boolean,
): {
content: string;
citations: string[];
inlineCitations?: XaiWebSearchResponse["inline_citations"];
} {
const { content, citations } = requireXaiResponseTextAndCitations(data, label);
return {
content,
citations,
inlineCitations:
inlineCitationsEnabled && Array.isArray(data.inline_citations)
? data.inline_citations
: undefined,
};
}
export const testing = {
buildXaiResponsesToolBody,
extractXaiWebSearchContent,
requireXaiResponseTextCitationsAndInline,
requireXaiResponseTextAndCitations,
resolveXaiResponseTextCitationsAndInline,
resolveXaiResponseTextAndCitations,
resolveXaiResponsesEndpoint,
XAI_RESPONSES_BASE_URL,
XAI_RESPONSES_ENDPOINT,
} as const;
export { testing as __testing };

View File

@@ -0,0 +1,328 @@
// Xai tests cover tool auth shared plugin behavior.
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
isXaiToolEnabled,
resolveFallbackXaiAuth,
resolveXaiToolApiKeyWithAuth,
} from "./tool-auth-shared.js";
describe("xai tool auth helpers", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("prefers plugin web search keys over legacy grok keys", () => {
expect(
resolveFallbackXaiAuth({
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "plugin-key", // pragma: allowlist secret
},
},
},
},
},
tools: {
web: {
search: {
grok: {
apiKey: "legacy-key", // pragma: allowlist secret
},
},
},
},
}),
).toEqual({
apiKey: "plugin-key",
source: "plugins.entries.xai.config.webSearch.apiKey",
});
});
it("returns source metadata and managed markers for fallback auth", () => {
expect(
resolveFallbackXaiAuth({
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/tool-key" },
},
},
},
},
},
}),
).toEqual({
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
});
expect(
resolveFallbackXaiAuth({
tools: {
web: {
search: {
grok: {
apiKey: "legacy-key", // pragma: allowlist secret
},
},
},
},
}),
).toEqual({
apiKey: "legacy-key",
source: "tools.web.search.grok.apiKey",
});
});
it("falls back to runtime, then source config, then env for tool auth", async () => {
vi.stubEnv("XAI_API_KEY", "env-key");
await expect(
resolveXaiToolApiKeyWithAuth({
runtimeConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "runtime-key", // pragma: allowlist secret
},
},
},
},
},
},
sourceConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "source-key", // pragma: allowlist secret
},
},
},
},
},
},
}),
).resolves.toBe("runtime-key");
await expect(
resolveXaiToolApiKeyWithAuth({
sourceConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "source-key", // pragma: allowlist secret
},
},
},
},
},
},
}),
).resolves.toBe("source-key");
await expect(resolveXaiToolApiKeyWithAuth({})).resolves.toBe("env-key");
});
it("honors explicit disabled flags before auth fallback", () => {
vi.stubEnv("XAI_API_KEY", "env-key");
expect(isXaiToolEnabled({ enabled: false })).toBe(false);
expect(isXaiToolEnabled({ enabled: true })).toBe(true);
});
it("uses xAI auth profiles when tool config and env are absent", async () => {
const auth = {
hasAuthForProvider: (providerId: string) => providerId === "xai",
resolveApiKeyForProvider: async (providerId: string) =>
providerId === "xai" ? "profile-key" : undefined, // pragma: allowlist secret
};
expect(isXaiToolEnabled({ auth })).toBe(true);
await expect(resolveXaiToolApiKeyWithAuth({ auth })).resolves.toBe("profile-key");
});
it("does not use env fallback when a non-env SecretRef is configured but unavailable", async () => {
vi.stubEnv("XAI_API_KEY", "env-key");
await expect(
resolveXaiToolApiKeyWithAuth({
sourceConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: {
source: "file",
provider: "vault",
id: "/xai/tool-key",
},
},
},
},
},
},
},
}),
).resolves.toBeUndefined();
});
it("does not bypass blocked explicit tool config with auth profiles", async () => {
const auth = {
hasAuthForProvider: (providerId: string) => providerId === "xai",
resolveApiKeyForProvider: async () => "profile-key", // pragma: allowlist secret
};
const sourceConfig = {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: {
source: "file",
provider: "vault",
id: "/xai/tool-key",
},
},
},
},
},
},
};
expect(isXaiToolEnabled({ sourceConfig, auth })).toBe(false);
await expect(resolveXaiToolApiKeyWithAuth({ sourceConfig, auth })).resolves.toBeUndefined();
});
it("resolves env SecretRefs from source config when runtime snapshot is unavailable", async () => {
vi.stubEnv("XAI_API_KEY", "xai-secretref-key");
await expect(
resolveXaiToolApiKeyWithAuth({
sourceConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: {
source: "env",
provider: "default",
id: "XAI_API_KEY",
},
},
},
},
},
},
},
}),
).resolves.toBe("xai-secretref-key");
});
it("does not read arbitrary env SecretRef ids for xAI tool auth", async () => {
vi.stubEnv("UNRELATED_SECRET", "should-not-be-read");
await expect(
resolveXaiToolApiKeyWithAuth({
sourceConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: {
source: "env",
provider: "default",
id: "UNRELATED_SECRET",
},
},
},
},
},
},
},
}),
).resolves.toBeUndefined();
});
it("does not resolve env SecretRefs when provider allowlist excludes XAI_API_KEY", async () => {
vi.stubEnv("XAI_API_KEY", "xai-secretref-key");
await expect(
resolveXaiToolApiKeyWithAuth({
sourceConfig: {
secrets: {
providers: {
"xai-env": {
source: "env",
allowlist: ["OTHER_XAI_API_KEY"],
},
},
},
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: {
source: "env",
provider: "xai-env",
id: "XAI_API_KEY",
},
},
},
},
},
},
},
}),
).resolves.toBeUndefined();
});
it("does not resolve env SecretRefs when provider source is not env", async () => {
vi.stubEnv("XAI_API_KEY", "xai-secretref-key");
await expect(
resolveXaiToolApiKeyWithAuth({
sourceConfig: {
secrets: {
providers: {
"xai-env": {
source: "file",
path: "/tmp/secrets.json",
},
},
},
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: {
source: "env",
provider: "xai-env",
id: "XAI_API_KEY",
},
},
},
},
},
},
},
}),
).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,194 @@
// Xai plugin module implements tool auth shared behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/extension-shared";
import {
coerceSecretRef,
resolveNonEnvSecretRefApiKeyMarker,
} from "openclaw/plugin-sdk/provider-auth";
import {
readProviderEnvValue,
resolveProviderWebSearchPluginConfig,
} from "openclaw/plugin-sdk/provider-web-search";
import {
normalizeSecretInputString,
resolveSecretInputString,
} from "openclaw/plugin-sdk/secret-input";
type XaiFallbackAuth = {
apiKey: string;
source: string;
};
const XAI_API_KEY_ENV_VAR = "XAI_API_KEY";
const XAI_PROVIDER_ID = "xai";
export type XaiToolAuthContext = {
hasAuthForProvider?: (providerId: string) => boolean;
resolveApiKeyForProvider?: (providerId: string) => Promise<string | undefined>;
};
type ConfiguredRuntimeApiKeyResolution =
| { status: "available"; value: string }
| { status: "missing" }
| { status: "blocked" };
function readConfiguredOrManagedApiKey(value: unknown): string | undefined {
const literal = normalizeSecretInputString(value);
if (literal) {
return literal;
}
const ref = coerceSecretRef(value);
return ref ? resolveNonEnvSecretRefApiKeyMarker(ref.source) : undefined;
}
function readLegacyGrokFallbackAuth(cfg?: OpenClawConfig): XaiFallbackAuth | undefined {
const search = cfg?.tools?.web?.search;
if (!search || typeof search !== "object") {
return undefined;
}
const grok = (search as Record<string, unknown>).grok;
const apiKey = readConfiguredOrManagedApiKey(
grok && typeof grok === "object" ? (grok as Record<string, unknown>).apiKey : undefined,
);
return apiKey ? { apiKey, source: "tools.web.search.grok.apiKey" } : undefined;
}
function readConfiguredRuntimeApiKey(
value: unknown,
path: string,
cfg?: OpenClawConfig,
): ConfiguredRuntimeApiKeyResolution {
const resolved = resolveSecretInputString({
value,
path,
defaults: cfg?.secrets?.defaults,
mode: "inspect",
});
if (resolved.status === "available") {
return { status: "available", value: resolved.value };
}
if (resolved.status === "missing") {
return { status: "missing" };
}
if (resolved.ref.source !== "env") {
return { status: "blocked" };
}
const envVarName = resolved.ref.id.trim();
if (envVarName !== XAI_API_KEY_ENV_VAR) {
return { status: "blocked" };
}
if (
!canResolveEnvSecretRefInReadOnlyPath({
cfg,
provider: resolved.ref.provider,
id: envVarName,
})
) {
return { status: "blocked" };
}
const envValue = normalizeSecretInputString(process.env[envVarName]);
return envValue ? { status: "available", value: envValue } : { status: "missing" };
}
function readLegacyGrokApiKeyResult(cfg?: OpenClawConfig): ConfiguredRuntimeApiKeyResolution {
const search = cfg?.tools?.web?.search;
if (!search || typeof search !== "object") {
return { status: "missing" };
}
const grok = (search as Record<string, unknown>).grok;
return readConfiguredRuntimeApiKey(
grok && typeof grok === "object" ? (grok as Record<string, unknown>).apiKey : undefined,
"tools.web.search.grok.apiKey",
cfg,
);
}
function readPluginXaiWebSearchApiKeyResult(
cfg?: OpenClawConfig,
): ConfiguredRuntimeApiKeyResolution {
return readConfiguredRuntimeApiKey(
resolveProviderWebSearchPluginConfig(cfg as Record<string, unknown> | undefined, "xai")?.apiKey,
"plugins.entries.xai.config.webSearch.apiKey",
cfg,
);
}
function resolveConfiguredXaiToolApiKeyResult(params: {
runtimeConfig?: OpenClawConfig;
sourceConfig?: OpenClawConfig;
}): ConfiguredRuntimeApiKeyResolution {
const runtimePlugin = readPluginXaiWebSearchApiKeyResult(params.runtimeConfig);
if (runtimePlugin.status === "available" || runtimePlugin.status === "blocked") {
return runtimePlugin;
}
const runtimeLegacy = readLegacyGrokApiKeyResult(params.runtimeConfig);
if (runtimeLegacy.status === "available" || runtimeLegacy.status === "blocked") {
return runtimeLegacy;
}
const sourcePlugin = readPluginXaiWebSearchApiKeyResult(params.sourceConfig);
if (sourcePlugin.status === "available" || sourcePlugin.status === "blocked") {
return sourcePlugin;
}
const sourceLegacy = readLegacyGrokApiKeyResult(params.sourceConfig);
if (sourceLegacy.status === "available" || sourceLegacy.status === "blocked") {
return sourceLegacy;
}
return { status: "missing" };
}
function hasXaiAuthProfile(auth?: XaiToolAuthContext): boolean {
return auth?.hasAuthForProvider?.(XAI_PROVIDER_ID) === true;
}
async function resolveXaiAuthProfileApiKey(auth?: XaiToolAuthContext): Promise<string | undefined> {
const value = await auth?.resolveApiKeyForProvider?.(XAI_PROVIDER_ID);
return normalizeSecretInputString(value);
}
export function resolveFallbackXaiAuth(cfg?: OpenClawConfig): XaiFallbackAuth | undefined {
const pluginApiKey = readConfiguredOrManagedApiKey(
resolveProviderWebSearchPluginConfig(cfg as Record<string, unknown> | undefined, "xai")?.apiKey,
);
if (pluginApiKey) {
return {
apiKey: pluginApiKey,
source: "plugins.entries.xai.config.webSearch.apiKey",
};
}
return readLegacyGrokFallbackAuth(cfg);
}
export async function resolveXaiToolApiKeyWithAuth(params: {
runtimeConfig?: OpenClawConfig;
sourceConfig?: OpenClawConfig;
auth?: XaiToolAuthContext;
}): Promise<string | undefined> {
const configured = resolveConfiguredXaiToolApiKeyResult(params);
if (configured.status === "available") {
return configured.value;
}
if (configured.status === "blocked") {
return undefined;
}
return (
(await resolveXaiAuthProfileApiKey(params.auth)) ?? readProviderEnvValue([XAI_API_KEY_ENV_VAR])
);
}
export function isXaiToolEnabled(params: {
enabled?: boolean;
runtimeConfig?: OpenClawConfig;
sourceConfig?: OpenClawConfig;
auth?: XaiToolAuthContext;
}): boolean {
if (params.enabled === false) {
return false;
}
const configured = resolveConfiguredXaiToolApiKeyResult(params);
if (configured.status === "available") {
return true;
}
if (configured.status === "blocked") {
return false;
}
return hasXaiAuthProfile(params.auth) || Boolean(readProviderEnvValue([XAI_API_KEY_ENV_VAR]));
}

View File

@@ -0,0 +1,37 @@
// Xai tests cover tool config shared plugin behavior.
import { describe, expect, it } from "vitest";
import {
coerceXaiToolConfig,
resolveNormalizedXaiToolModel,
resolvePositiveIntegerToolConfig,
} from "./tool-config-shared.js";
describe("xai tool config helpers", () => {
it("coerces non-record config to an empty object", () => {
expect(coerceXaiToolConfig(undefined)).toStrictEqual({});
expect(coerceXaiToolConfig([] as unknown as Record<string, unknown>)).toStrictEqual({});
});
it("normalizes configured model ids and falls back to the default model", () => {
expect(
resolveNormalizedXaiToolModel({
config: { model: " grok-4.1-fast " },
defaultModel: "grok-4-1-fast",
}),
).toBe("grok-4.1-fast");
expect(
resolveNormalizedXaiToolModel({
config: {},
defaultModel: "grok-4-1-fast",
}),
).toBe("grok-4-1-fast");
});
it("accepts only positive finite numeric turn counts", () => {
expect(resolvePositiveIntegerToolConfig({ maxTurns: 2.9 }, "maxTurns")).toBe(2);
expect(resolvePositiveIntegerToolConfig({ maxTurns: 0 }, "maxTurns")).toBeUndefined();
expect(resolvePositiveIntegerToolConfig({ maxTurns: Number.NaN }, "maxTurns")).toBeUndefined();
expect(resolvePositiveIntegerToolConfig(undefined, "maxTurns")).toBeUndefined();
});
});

View File

@@ -0,0 +1,33 @@
// Xai helper module supports tool config shared behavior.
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeXaiModelId } from "../model-id.js";
export { isRecord };
export function coerceXaiToolConfig(
config: Record<string, unknown> | undefined,
): Record<string, unknown> {
return isRecord(config) ? config : {};
}
export function resolveNormalizedXaiToolModel(params: {
config?: Record<string, unknown>;
defaultModel: string;
}): string {
const value = coerceXaiToolConfig(params.config).model;
return typeof value === "string" && value.trim()
? normalizeXaiModelId(value.trim())
: params.defaultModel;
}
export function resolvePositiveIntegerToolConfig(
config: Record<string, unknown> | undefined,
key: string,
): number | undefined {
const raw = coerceXaiToolConfig(config)[key];
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return undefined;
}
const normalized = Math.trunc(raw);
return normalized > 0 ? normalized : undefined;
}

View File

@@ -0,0 +1,433 @@
// Xai provider module implements model/runtime integration.
import { resolveDefaultAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
coerceSecretRef,
ensureAuthProfileStore,
listUsableProviderAuthProfileIds,
} from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
DEFAULT_CACHE_TTL_MINUTES,
formatCliCommand,
getScopedCredentialValue,
mergeScopedSearchConfig,
normalizeCacheKey,
readCache,
readPositiveIntegerParam,
readStringParam,
resolveCacheTtlMs,
resolveProviderWebSearchPluginConfig,
resolveTimeoutSeconds,
resolveWebSearchProviderCredential,
type WebSearchProviderSetupContext,
writeCache,
} from "openclaw/plugin-sdk/provider-web-search";
import {
buildXaiWebSearchPayload,
extractXaiWebSearchContent,
requestXaiWebSearch,
resolveXaiInlineCitations,
resolveXaiWebSearchEndpoint,
resolveXaiWebSearchModel,
} from "./web-search-shared.js";
import { resolveEffectiveXSearchConfig, setPluginXSearchConfigValue } from "./x-search-config.js";
import { XAI_DEFAULT_X_SEARCH_MODEL } from "./x-search-shared.js";
const XAI_WEB_SEARCH_CACHE = new Map<
string,
{ value: Record<string, unknown>; insertedAt: number; expiresAt: number }
>();
const XAI_WEB_SEARCH_DEFAULT_TIMEOUT_SECONDS = 60;
const XAI_PROVIDER_ID = "xai";
const X_SEARCH_MODEL_OPTIONS = [
{
value: XAI_DEFAULT_X_SEARCH_MODEL,
label: XAI_DEFAULT_X_SEARCH_MODEL,
hint: "default · fast, no reasoning",
},
{
value: "grok-4-1-fast",
label: "grok-4-1-fast",
hint: "fast with reasoning",
},
] as const;
function resolveXSearchConfigRecord(
config?: WebSearchProviderSetupContext["config"],
): Record<string, unknown> | undefined {
return resolveEffectiveXSearchConfig(config);
}
export async function runXaiSearchProviderSetup(
ctx: WebSearchProviderSetupContext,
): Promise<WebSearchProviderSetupContext["config"]> {
const existingXSearch = resolveXSearchConfigRecord(ctx.config);
if (existingXSearch?.enabled === false) {
return ctx.config;
}
await ctx.prompter.note(
[
"x_search lets your agent search X (formerly Twitter) posts via xAI.",
"It reuses the same xAI credential you configured for Grok web search.",
`You can change this later with ${formatCliCommand("openclaw configure --section web")}.`,
].join("\n"),
"X search",
);
const enableChoice = await ctx.prompter.select<"yes" | "skip">({
message: "Enable x_search too?",
options: [
{
value: "yes",
label: "Yes, enable x_search",
hint: "Search X posts with the same xAI credential",
},
{
value: "skip",
label: "Skip for now",
hint: "Keep Grok web_search only",
},
],
initialValue: existingXSearch?.enabled === true || ctx.quickstartDefaults ? "yes" : "skip",
});
if (enableChoice === "skip") {
return ctx.config;
}
const existingModel =
typeof existingXSearch?.model === "string" && existingXSearch.model.trim()
? existingXSearch.model.trim()
: "";
const knownModel = X_SEARCH_MODEL_OPTIONS.find((entry) => entry.value === existingModel)?.value;
const modelPick = await ctx.prompter.select<string>({
message: "Grok model for x_search",
options: [
...X_SEARCH_MODEL_OPTIONS,
{ value: "__custom__", label: "Enter custom model name", hint: "" },
],
initialValue: knownModel ?? XAI_DEFAULT_X_SEARCH_MODEL,
});
let model = modelPick;
if (modelPick === "__custom__") {
const customModel = await ctx.prompter.text({
message: "Custom Grok model name",
initialValue: existingModel || XAI_DEFAULT_X_SEARCH_MODEL,
placeholder: XAI_DEFAULT_X_SEARCH_MODEL,
});
model = customModel.trim() || XAI_DEFAULT_X_SEARCH_MODEL;
}
const next = structuredClone(ctx.config);
setPluginXSearchConfigValue(next, "enabled", true);
setPluginXSearchConfigValue(next, "model", model || XAI_DEFAULT_X_SEARCH_MODEL);
return next;
}
function runXaiWebSearch(params: {
query: string;
model: string;
endpoint: string;
apiKey: string;
timeoutSeconds: number;
inlineCitations: boolean;
cacheTtlMs: number;
}): Promise<Record<string, unknown>> {
const cacheKey = normalizeCacheKey(
`grok:${params.endpoint}:${params.model}:${String(params.inlineCitations)}:${params.query}`,
);
const cached = readCache(XAI_WEB_SEARCH_CACHE, cacheKey);
if (cached) {
return Promise.resolve({ ...cached.value, cached: true });
}
return (async () => {
const startedAt = Date.now();
const result = await requestXaiWebSearch({
query: params.query,
model: params.model,
apiKey: params.apiKey,
endpoint: params.endpoint,
timeoutSeconds: params.timeoutSeconds,
inlineCitations: params.inlineCitations,
});
const payload = buildXaiWebSearchPayload({
query: params.query,
provider: "grok",
model: params.model,
tookMs: Date.now() - startedAt,
content: result.content,
citations: result.citations,
inlineCitations: result.inlineCitations,
});
writeCache(XAI_WEB_SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
return payload;
})();
}
function resolveXaiToolSearchConfig(ctx: {
config?: Record<string, unknown>;
searchConfig?: Record<string, unknown>;
}) {
return mergeScopedSearchConfig(
ctx.searchConfig,
"grok",
resolveProviderWebSearchPluginConfig(ctx.config, "xai"),
);
}
function resolveXaiWebSearchCredential(searchConfig?: Record<string, unknown>): string | undefined {
return resolveWebSearchProviderCredential({
credentialValue: getScopedCredentialValue(searchConfig, "grok"),
path: "tools.web.search.grok.apiKey",
envVars: ["XAI_API_KEY"],
});
}
function resolveConfiguredXaiWebSearchCredential(
searchConfig?: Record<string, unknown>,
): string | undefined {
return resolveWebSearchProviderCredential({
credentialValue: getScopedCredentialValue(searchConfig, "grok"),
path: "tools.web.search.grok.apiKey",
envVars: [],
});
}
function hasConfiguredXaiWebSearchCredentialRef(searchConfig?: Record<string, unknown>): boolean {
return coerceSecretRef(getScopedCredentialValue(searchConfig, "grok")) !== null;
}
type XaiResolvedWebSearchAuth = {
apiKey: string;
mode?: "api-key" | "oauth" | "token" | "aws-sdk";
profileId?: string;
};
async function resolveXaiProviderAuthCredential(params: {
config?: Record<string, unknown>;
agentDir?: string;
credentialPrecedence?: "profile-first" | "env-first";
forceRefresh?: boolean;
profileId?: string;
}): Promise<XaiResolvedWebSearchAuth | undefined> {
try {
const config = params.config as OpenClawConfig | undefined;
const agentDir =
params.agentDir?.trim() || (config ? resolveDefaultAgentDir(config) : undefined);
const resolved = await resolveApiKeyForProvider({
provider: XAI_PROVIDER_ID,
cfg: config,
...(agentDir ? { agentDir } : {}),
...(params.profileId
? {
profileId: params.profileId,
lockedProfile: true,
}
: {}),
...(params.forceRefresh ? { forceRefresh: true } : {}),
...(params.credentialPrecedence ? { credentialPrecedence: params.credentialPrecedence } : {}),
});
const apiKey = typeof resolved.apiKey === "string" ? resolved.apiKey.trim() : "";
if (!apiKey) {
return undefined;
}
return {
apiKey,
mode: resolved.mode,
...(resolved.profileId ? { profileId: resolved.profileId } : {}),
};
} catch {
return undefined;
}
}
async function resolveXaiProviderApiKeyProfileFallback(params: {
config?: Record<string, unknown>;
agentDir?: string;
}): Promise<XaiResolvedWebSearchAuth | undefined> {
const config = params.config as OpenClawConfig | undefined;
const usableProfiles = listUsableProviderAuthProfileIds({
agentDir: params.agentDir,
cfg: config,
provider: XAI_PROVIDER_ID,
});
if (!usableProfiles.agentDir || usableProfiles.profileIds.length === 0) {
return undefined;
}
const store = ensureAuthProfileStore(usableProfiles.agentDir, {
allowKeychainPrompt: false,
});
for (const profileId of usableProfiles.profileIds) {
const profile = store.profiles[profileId];
if (!profile || profile.provider !== XAI_PROVIDER_ID || profile.type === "oauth") {
continue;
}
const resolved = await resolveXaiProviderAuthCredential({
agentDir: usableProfiles.agentDir,
config: params.config,
profileId,
});
if (resolved?.apiKey && resolved.mode !== "oauth") {
return resolved;
}
}
return undefined;
}
async function resolveXaiWebSearchAuth(
ctx: { config?: Record<string, unknown>; agentDir?: string },
searchConfig?: Record<string, unknown>,
options?: { forceRefresh?: boolean; profileId?: string },
): Promise<XaiResolvedWebSearchAuth | undefined> {
const providerAuth = await resolveXaiProviderAuthCredential({
agentDir: ctx.agentDir,
config: ctx.config,
forceRefresh: options?.forceRefresh,
profileId: options?.profileId,
});
if (providerAuth?.mode === "oauth") {
return providerAuth;
}
const configured = resolveConfiguredXaiWebSearchCredential(searchConfig);
if (configured) {
return {
apiKey: configured,
mode: "api-key",
};
}
if (hasConfiguredXaiWebSearchCredentialRef(searchConfig)) {
return undefined;
}
return providerAuth;
}
async function resolveXaiWebSearchApiKeyFallback(
ctx: { config?: Record<string, unknown>; agentDir?: string },
searchConfig?: Record<string, unknown>,
): Promise<XaiResolvedWebSearchAuth | undefined> {
const configured = resolveConfiguredXaiWebSearchCredential(searchConfig);
if (configured) {
return {
apiKey: configured,
mode: "api-key",
};
}
if (hasConfiguredXaiWebSearchCredentialRef(searchConfig)) {
return undefined;
}
const providerAuth = await resolveXaiProviderAuthCredential({
agentDir: ctx.agentDir,
config: ctx.config,
credentialPrecedence: "env-first",
});
if (providerAuth?.apiKey && providerAuth.mode !== "oauth") {
return providerAuth;
}
return await resolveXaiProviderApiKeyProfileFallback({
agentDir: ctx.agentDir,
config: ctx.config,
});
}
function isXaiUnauthorizedError(error: unknown): boolean {
return error instanceof Error && error.message.includes("xAI API error (401)");
}
function resolveXaiWebSearchTimeoutSeconds(searchConfig?: Record<string, unknown>): number {
return resolveTimeoutSeconds(
searchConfig?.timeoutSeconds,
XAI_WEB_SEARCH_DEFAULT_TIMEOUT_SECONDS,
);
}
export async function executeXaiWebSearchProviderTool(
ctx: {
config?: Record<string, unknown>;
searchConfig?: Record<string, unknown>;
agentDir?: string;
},
args: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const searchConfig = resolveXaiToolSearchConfig(ctx);
const auth = await resolveXaiWebSearchAuth(ctx, searchConfig);
if (!auth) {
return {
error: "missing_xai_api_key",
message:
"web_search (grok) needs xAI credentials. Run `openclaw onboard --auth-choice xai-oauth` to sign in with Grok, run `openclaw onboard --auth-choice xai-api-key`, set `XAI_API_KEY` in the Gateway environment, or configure `plugins.entries.xai.config.webSearch.apiKey`. If you do not want to configure search credentials, use web_fetch for a specific URL or the browser tool for interactive pages.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
const query = readStringParam(args, "query", { required: true });
void readPositiveIntegerParam(args, "count", {
max: 10,
message: "count must be an integer from 1 to 10.",
});
const request = {
query,
model: resolveXaiWebSearchModel(searchConfig),
endpoint: resolveXaiWebSearchEndpoint(searchConfig),
timeoutSeconds: resolveXaiWebSearchTimeoutSeconds(searchConfig),
inlineCitations: resolveXaiInlineCitations(searchConfig),
cacheTtlMs: resolveCacheTtlMs(searchConfig?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES),
};
try {
return await runXaiWebSearch({
...request,
apiKey: auth.apiKey,
});
} catch (error) {
if (!isXaiUnauthorizedError(error) || !auth.profileId) {
throw error;
}
if (auth.mode === "oauth") {
const refreshed = await resolveXaiWebSearchAuth(ctx, searchConfig, {
forceRefresh: true,
profileId: auth.profileId,
});
if (refreshed?.apiKey && refreshed.apiKey !== auth.apiKey) {
return await runXaiWebSearch({
...request,
apiKey: refreshed.apiKey,
});
}
}
const fallback = await resolveXaiWebSearchApiKeyFallback(ctx, searchConfig);
if (!fallback?.apiKey || fallback.apiKey === auth.apiKey) {
throw error;
}
return await runXaiWebSearch({
...request,
apiKey: fallback.apiKey,
});
}
}
export const testing = {
buildXaiWebSearchPayload,
extractXaiWebSearchContent,
resolveXaiToolSearchConfig,
resolveXaiWebSearchAuth,
resolveXaiInlineCitations,
resolveXaiWebSearchCredential,
resolveXaiWebSearchEndpoint,
resolveXaiWebSearchModel,
resolveXaiWebSearchTimeoutSeconds,
requestXaiWebSearch,
};
export { testing as __testing };

View File

@@ -0,0 +1,26 @@
// Xai type declarations define plugin contracts.
export type XaiWebSearchResponse = {
output?: Array<{
type?: string;
text?: string;
content?: Array<{
type?: string;
text?: string;
annotations?: Array<{
type?: string;
url?: string;
} | null>;
} | null>;
annotations?: Array<{
type?: string;
url?: string;
} | null>;
} | null>;
output_text?: string;
citations?: string[];
inline_citations?: Array<{
start_index: number;
end_index: number;
url: string;
}>;
};

View File

@@ -0,0 +1,125 @@
// Xai plugin module implements web search shared behavior.
import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http";
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
import { normalizeXaiModelId } from "../model-id.js";
import {
buildXaiResponsesToolBody,
requireXaiResponseTextCitationsAndInline,
resolveXaiResponsesEndpoint,
} from "./responses-tool-shared.js";
import { isRecord } from "./tool-config-shared.js";
import type { XaiWebSearchResponse } from "./web-search-response.types.js";
export { extractXaiWebSearchContent } from "./responses-tool-shared.js";
export type { XaiWebSearchResponse } from "./web-search-response.types.js";
const XAI_DEFAULT_WEB_SEARCH_MODEL = "grok-4-1-fast";
type XaiWebSearchConfig = Record<string, unknown> & {
baseUrl?: unknown;
model?: unknown;
inlineCitations?: unknown;
};
type XaiWebSearchResult = {
content: string;
citations: string[];
inlineCitations?: XaiWebSearchResponse["inline_citations"];
};
export function buildXaiWebSearchPayload(params: {
query: string;
provider: string;
model: string;
tookMs: number;
content: string;
citations: string[];
inlineCitations?: XaiWebSearchResponse["inline_citations"];
}): Record<string, unknown> {
return {
query: params.query,
provider: params.provider,
model: params.model,
tookMs: params.tookMs,
externalContent: {
untrusted: true,
source: "web_search",
provider: params.provider,
wrapped: true,
},
content: wrapWebContent(params.content, "web_search"),
citations: params.citations,
...(params.inlineCitations ? { inlineCitations: params.inlineCitations } : {}),
};
}
function resolveXaiSearchConfig(searchConfig?: Record<string, unknown>): XaiWebSearchConfig {
return (
(isRecord(searchConfig?.grok) ? (searchConfig.grok as XaiWebSearchConfig) : undefined) ?? {}
);
}
export function resolveXaiWebSearchModel(searchConfig?: Record<string, unknown>): string {
const config = resolveXaiSearchConfig(searchConfig);
return typeof config.model === "string" && config.model.trim()
? normalizeXaiModelId(config.model.trim())
: XAI_DEFAULT_WEB_SEARCH_MODEL;
}
export function resolveXaiWebSearchEndpoint(searchConfig?: Record<string, unknown>): string {
return resolveXaiResponsesEndpoint(resolveXaiSearchConfig(searchConfig).baseUrl);
}
export function resolveXaiInlineCitations(searchConfig?: Record<string, unknown>): boolean {
return resolveXaiSearchConfig(searchConfig).inlineCitations === true;
}
function isAbortError(error: unknown): boolean {
return (
error instanceof Error &&
(error.name === "AbortError" || error.message === "This operation was aborted")
);
}
export function wrapXaiWebSearchError(error: unknown, timeoutSeconds: number): never {
if (isAbortError(error)) {
throw new Error(
`xAI web search timed out after ${timeoutSeconds}s. Increase tools.web.search.timeoutSeconds if queries are complex.`,
{ cause: error },
);
}
throw error;
}
export async function requestXaiWebSearch(params: {
query: string;
model: string;
apiKey: string;
endpoint: string;
timeoutSeconds: number;
inlineCitations: boolean;
}): Promise<XaiWebSearchResult> {
return await postTrustedWebToolsJson(
{
url: params.endpoint,
timeoutSeconds: params.timeoutSeconds,
apiKey: params.apiKey,
body: buildXaiResponsesToolBody({
model: params.model,
inputText: params.query,
tools: [{ type: "web_search" }],
}),
errorLabel: "xAI",
},
async (response) => {
const data = (await readProviderJsonObjectResponse(
response,
"xAI web search failed",
)) as XaiWebSearchResponse;
return requireXaiResponseTextCitationsAndInline(
data,
"xAI web search failed",
params.inlineCitations,
);
},
).catch((error: unknown) => wrapXaiWebSearchError(error, params.timeoutSeconds));
}

View File

@@ -0,0 +1,79 @@
// Xai helper module supports x search config behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { isRecord } from "./tool-config-shared.js";
type JsonRecord = Record<string, unknown>;
function cloneRecord<T extends JsonRecord | undefined>(value: T): T {
if (!value) {
return value;
}
return { ...value } as T;
}
function resolveLegacyXSearchConfig(config?: OpenClawConfig): JsonRecord | undefined {
const web = config?.tools?.web as Record<string, unknown> | undefined;
const xSearch = web?.x_search;
return isRecord(xSearch) ? cloneRecord(xSearch) : undefined;
}
function resolvePluginXSearchConfig(config?: OpenClawConfig): JsonRecord | undefined {
const pluginConfig = config?.plugins?.entries?.xai?.config;
if (!isRecord(pluginConfig?.xSearch)) {
return undefined;
}
return cloneRecord(pluginConfig.xSearch);
}
function resolveLegacyGrokWebSearchConfig(config?: OpenClawConfig): JsonRecord | undefined {
const web = config?.tools?.web as Record<string, unknown> | undefined;
const search = web?.search;
if (!isRecord(search) || !isRecord(search.grok)) {
return undefined;
}
return cloneRecord(search.grok);
}
function resolvePluginWebSearchConfig(config?: OpenClawConfig): JsonRecord | undefined {
const pluginConfig = config?.plugins?.entries?.xai?.config;
if (!isRecord(pluginConfig?.webSearch)) {
return undefined;
}
return cloneRecord(pluginConfig.webSearch);
}
function baseUrlFallback(config?: JsonRecord): JsonRecord | undefined {
return typeof config?.baseUrl === "string" && config.baseUrl.trim()
? { baseUrl: config.baseUrl }
: undefined;
}
export function resolveEffectiveXSearchConfig(config?: OpenClawConfig): JsonRecord | undefined {
const legacyGrokBaseUrl = baseUrlFallback(resolveLegacyGrokWebSearchConfig(config));
const pluginWebSearchBaseUrl = baseUrlFallback(resolvePluginWebSearchConfig(config));
const legacy = resolveLegacyXSearchConfig(config);
const pluginOwned = resolvePluginXSearchConfig(config);
const merged = {
...legacyGrokBaseUrl,
...pluginWebSearchBaseUrl,
...legacy,
...pluginOwned,
};
if (Object.keys(merged).length === 0) {
return undefined;
}
return merged;
}
export function setPluginXSearchConfigValue(
configTarget: OpenClawConfig,
key: string,
value: unknown,
): void {
const plugins = (configTarget.plugins ??= {}) as { entries?: Record<string, unknown> };
const entries = (plugins.entries ??= {});
const entry = (entries.xai ??= {}) as { config?: Record<string, unknown> };
const config = (entry.config ??= {});
const xSearch = (config.xSearch ??= {}) as Record<string, unknown>;
xSearch[key] = value;
}

View File

@@ -0,0 +1,147 @@
// Xai plugin module implements x search shared behavior.
import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http";
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
import {
buildXaiResponsesToolBody,
requireXaiResponseTextCitationsAndInline,
resolveXaiResponsesEndpoint,
} from "./responses-tool-shared.js";
import {
coerceXaiToolConfig,
resolveNormalizedXaiToolModel,
resolvePositiveIntegerToolConfig,
} from "./tool-config-shared.js";
import type { XaiWebSearchResponse } from "./web-search-shared.js";
export const XAI_DEFAULT_X_SEARCH_MODEL = "grok-4-1-fast-non-reasoning";
type XaiXSearchConfig = {
apiKey?: unknown;
baseUrl?: unknown;
model?: unknown;
inlineCitations?: unknown;
maxTurns?: unknown;
};
export type XaiXSearchOptions = {
query: string;
allowedXHandles?: string[];
excludedXHandles?: string[];
fromDate?: string;
toDate?: string;
enableImageUnderstanding?: boolean;
enableVideoUnderstanding?: boolean;
};
type XaiXSearchResult = {
content: string;
citations: string[];
inlineCitations?: XaiWebSearchResponse["inline_citations"];
};
function resolveXaiXSearchConfig(config?: Record<string, unknown>): XaiXSearchConfig {
return coerceXaiToolConfig(config) as XaiXSearchConfig;
}
export function resolveXaiXSearchModel(config?: Record<string, unknown>): string {
return resolveNormalizedXaiToolModel({
config,
defaultModel: XAI_DEFAULT_X_SEARCH_MODEL,
});
}
export function resolveXaiXSearchEndpoint(config?: Record<string, unknown>): string {
return resolveXaiResponsesEndpoint(resolveXaiXSearchConfig(config).baseUrl);
}
export function resolveXaiXSearchInlineCitations(config?: Record<string, unknown>): boolean {
return resolveXaiXSearchConfig(config).inlineCitations === true;
}
export function resolveXaiXSearchMaxTurns(config?: Record<string, unknown>): number | undefined {
return resolvePositiveIntegerToolConfig(config, "maxTurns");
}
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;
endpoint: string;
model: string;
timeoutSeconds: number;
inlineCitations: boolean;
maxTurns?: number;
options: XaiXSearchOptions;
}): Promise<XaiXSearchResult> {
return await postTrustedWebToolsJson(
{
url: params.endpoint,
timeoutSeconds: params.timeoutSeconds,
apiKey: params.apiKey,
body: buildXaiResponsesToolBody({
model: params.model,
inputText: params.options.query,
tools: [buildXSearchTool(params.options)],
maxTurns: params.maxTurns,
}),
errorLabel: "xAI",
},
async (response) => {
const data = (await readProviderJsonObjectResponse(
response,
"xAI X search failed",
)) as XaiWebSearchResponse;
return requireXaiResponseTextCitationsAndInline(
data,
"xAI X search failed",
params.inlineCitations,
);
},
);
}

View File

@@ -0,0 +1,60 @@
// Xai tests cover xai user agent plugin behavior.
import { afterEach, describe, expect, it, vi } from "vitest";
import { xaiUserAgent, xaiUserAgentHeaderFor } from "./xai-user-agent.js";
describe("xaiUserAgent", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("prefers OPENCLAW_VERSION env over the bundled package version", () => {
vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
expect(xaiUserAgent()).toBe("openclaw/2026.3.22");
});
it("falls back to OPENCLAW_SERVICE_VERSION when OPENCLAW_VERSION is unset", () => {
vi.stubEnv("OPENCLAW_VERSION", "");
vi.stubEnv("OPENCLAW_SERVICE_VERSION", "2026.3.99");
// OPENCLAW_VERSION from the SDK is the bundled VERSION constant. In a dev
// checkout it resolves to a real semver, so we cannot deterministically
// assert "unknown" here. We just lock the prefix to ensure the env-first
// contract holds whenever the bundle resolves to 0.0.0/empty.
const result = xaiUserAgent();
expect(result.startsWith("openclaw/")).toBe(true);
expect(result).not.toBe("openclaw/");
});
it("returns the openclaw/<version> shape", () => {
vi.stubEnv("OPENCLAW_VERSION", "2026.5.16");
expect(xaiUserAgent()).toMatch(/^openclaw\/\d+\.\d+\.\d+$/u);
});
});
describe("xaiUserAgentHeaderFor", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("emits User-Agent for the xAI-native host", () => {
vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
expect(xaiUserAgentHeaderFor("https://api.x.ai/v1")).toEqual({
"User-Agent": "openclaw/2026.3.22",
});
expect(xaiUserAgentHeaderFor("https://api.x.ai/v1/tts")).toEqual({
"User-Agent": "openclaw/2026.3.22",
});
});
it("withholds User-Agent on user-configured proxy baseUrls", () => {
vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
expect(xaiUserAgentHeaderFor("https://my-corp.proxy/xai/v1")).toEqual({});
expect(xaiUserAgentHeaderFor("http://127.0.0.1:8080/v1")).toEqual({});
expect(xaiUserAgentHeaderFor("https://api.grok.x.ai/v1")).toEqual({});
});
it("returns an empty record for missing or invalid input", () => {
expect(xaiUserAgentHeaderFor(undefined)).toEqual({});
expect(xaiUserAgentHeaderFor("")).toEqual({});
expect(xaiUserAgentHeaderFor("not a url")).toEqual({});
});
});

View File

@@ -0,0 +1,52 @@
// Shared User-Agent for xAI sidecar HTTP/WS requests; mirrors `formatOpenClawUserAgent`.
import { OPENCLAW_VERSION as PACKAGE_VERSION } from "openclaw/plugin-sdk/agent-harness-runtime";
const ORIGINATOR = "openclaw";
const UNUSABLE_PACKAGE_VERSION = "0.0.0";
const FALLBACK_VERSION = "unknown";
function trimToUndefined(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed && trimmed.length > 0 ? trimmed : undefined;
}
function resolveXaiUserAgentVersion(): string {
// Env-first matches resolveRuntimeServiceVersion.
const envVersion = trimToUndefined(process.env.OPENCLAW_VERSION);
if (envVersion) {
return envVersion;
}
const packageVersion = trimToUndefined(PACKAGE_VERSION);
if (packageVersion && packageVersion !== UNUSABLE_PACKAGE_VERSION) {
return packageVersion;
}
return (
trimToUndefined(process.env.OPENCLAW_SERVICE_VERSION) ??
trimToUndefined(process.env.npm_package_version) ??
FALLBACK_VERSION
);
}
export function xaiUserAgent(): string {
return `${ORIGINATOR}/${resolveXaiUserAgentVersion()}`;
}
const XAI_NATIVE_API_HOSTS = new Set(["api.x.ai"]);
// Returns a `User-Agent` header entry only when the resolved baseUrl points
// at a verified xAI-native API host. User-configured proxy baseUrls produce
// an empty record so the openclaw identity is not forwarded to the proxy.
export function xaiUserAgentHeaderFor(baseUrl: string | undefined): Record<string, string> {
if (!baseUrl) {
return {};
}
try {
if (XAI_NATIVE_API_HOSTS.has(new URL(baseUrl).hostname)) {
return { "User-Agent": xaiUserAgent() };
}
} catch {
return {};
}
return {};
}