Skip to content

Commit 97944aa

Browse files
committed
fix: align xai tool auth profiles
1 parent c0132e9 commit 97944aa

16 files changed

Lines changed: 328 additions & 82 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ Docs: https://docs.openclaw.ai
191191
- Gateway/sessions: rotate generated transcript paths when gateway sessions reset, complementing the daily-rollover transcript persistence. (#79076) Thanks @vincentkoc.
192192
- Dependencies: pin the transitive `fast-uri` production dependency to `3.1.2` so the production dependency audit no longer resolves the vulnerable `<=3.1.1` range. Thanks @shakkernerd.
193193
- Plugins/install: fail managed npm plugin installs when OpenClaw cannot repair a required plugin-local `node_modules/openclaw` peer link, preventing that peer-link failure mode from producing unusable `@openclaw/codex` installs. Refs #79462. Thanks @ai-hpc.
194+
- xAI/tools: register and execute `x_search` and `code_execution` when the xAI API key comes from an auth profile, keeping the plugin tool gate aligned with `openclaw onboard --auth-choice xai-api-key`. Fixes #79353. Thanks @dbernaltbn.
194195
- Cron/agents: recognize same-target `edit`↔`write` recovery in `isSameToolMutationAction`, so a successful `write` to a path clears an earlier failed `edit` on the same path. Stops cron from reporting fatal failures when an agent self-heals across `edit` and `write`, while preserving same-tool fingerprint matching, blocking different-target writes, and excluding tools (including `apply_patch`) whose real call args do not produce a stable `path` fingerprint segment. Fixes #79024. Thanks @RenzoMXD.
195196
- Gateway/Tailscale: add opt-in `gateway.tailscale.preserveFunnel` so when `tailscale.mode = "serve"` and an externally configured Tailscale Funnel route already covers the gateway port, OpenClaw skips re-applying `tailscale serve` on startup and skips the `resetOnExit` teardown for that run, keeping operator-managed Funnel exposure alive across gateway restarts. Fixes #57241. Thanks @RenzoMXD.
196197
- CLI/router: when `openclaw <name>` does not match a CLI subcommand, check plugin tool manifests first so names like `lcm_recent` get an agent-tool diagnostic instead of the misleading suggestion to add the tool name to `plugins.allow`. Fixes #77214. Thanks @100yenadmin.

docs/providers/xai.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ OpenClaw ships a bundled `xai` provider plugin for Grok models.
3333

3434
<Note>
3535
OpenClaw uses the xAI Responses API as the bundled xAI transport. The same
36-
`XAI_API_KEY` can also power Grok-backed `web_search`, first-class `x_search`,
37-
and remote `code_execution`.
36+
API key from `openclaw onboard --auth-choice xai-api-key` can also power
37+
first-class `x_search` and remote `code_execution`; `XAI_API_KEY` or plugin
38+
web-search config can power Grok-backed `web_search` too.
3839
If you store an xAI key under `plugins.entries.xai.config.webSearch.apiKey`,
3940
the bundled xAI model provider reuses that key as a fallback too.
4041
Set `plugins.entries.xai.config.webSearch.baseUrl` to route Grok `web_search`
@@ -122,7 +123,8 @@ Legacy aliases still normalize to the canonical bundled ids:
122123

123124
<AccordionGroup>
124125
<Accordion title="Web search">
125-
The bundled `grok` web-search provider uses `XAI_API_KEY` too:
126+
The bundled `grok` web-search provider can use `XAI_API_KEY` or a plugin
127+
web-search key:
126128

127129
```bash
128130
openclaw config set tools.web.search.provider grok
@@ -409,8 +411,9 @@ Legacy aliases still normalize to the canonical bundled ids:
409411
</Accordion>
410412

411413
<Accordion title="Known limits">
412-
- Auth is API-key only today. There is no xAI OAuth or device-code flow in
413-
OpenClaw yet.
414+
- Auth is API-key only today. The API key may be stored in an xAI auth
415+
profile, environment variable, or plugin config; there is no xAI OAuth or
416+
device-code flow in OpenClaw yet.
414417
- `grok-4.20-multi-agent-experimental-beta-0304` is not supported on the
415418
normal xAI provider path because it requires a different upstream API
416419
surface than the standard OpenClaw xAI transport.

docs/tools/code-execution.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ title: "Code execution"
99

1010
`code_execution` runs sandboxed remote Python analysis on xAI's Responses API. It is registered by the bundled `xai` plugin (under the `tools` contract) and dispatches to the same `https://api.x.ai/v1/responses` endpoint used by `x_search`.
1111

12-
| Property | Value |
13-
| ------------------ | -------------------------------------------------------------- |
14-
| Tool name | `code_execution` |
15-
| Provider plugin | `xai` (bundled, `enabledByDefault: true`) |
16-
| Auth | `XAI_API_KEY` or `plugins.entries.xai.config.webSearch.apiKey` |
17-
| Default model | `grok-4-1-fast` |
18-
| Default timeout | 30 seconds |
19-
| Default `maxTurns` | unset (xAI applies its own internal limit) |
12+
| Property | Value |
13+
| ------------------ | --------------------------------------------------------------------------------- |
14+
| Tool name | `code_execution` |
15+
| Provider plugin | `xai` (bundled, `enabledByDefault: true`) |
16+
| Auth | xAI auth profile, `XAI_API_KEY`, or `plugins.entries.xai.config.webSearch.apiKey` |
17+
| Default model | `grok-4-1-fast` |
18+
| Default timeout | 30 seconds |
19+
| Default `maxTurns` | unset (xAI applies its own internal limit) |
2020

2121
This is different from local [`exec`](/tools/exec):
2222

@@ -37,7 +37,9 @@ Do **not** use it when you need local files, your shell, your repo, or paired de
3737

3838
<Steps>
3939
<Step title="Provide an xAI API key">
40-
Set `XAI_API_KEY` in the gateway environment, or configure the key under the xAI plugin so the same credential covers `code_execution`, `x_search`, web search, and other xAI tools:
40+
Run `openclaw onboard --auth-choice xai-api-key` for `code_execution` and
41+
`x_search`, or set `XAI_API_KEY` / configure the key under the xAI plugin
42+
when you also want Grok web search to use the same credential:
4143

4244
```bash
4345
export XAI_API_KEY=xai-...
@@ -117,12 +119,12 @@ The tool takes a single `task` parameter internally, so the agent should send th
117119

118120
## Errors
119121

120-
When the tool runs without auth, it returns a structured `missing_xai_api_key` error pointing at the env var and config path. The error is JSON, not a thrown exception, so the agent can self-correct:
122+
When the tool runs without auth, it returns a structured `missing_xai_api_key` error pointing at the auth-profile, env var, and config options. The error is JSON, not a thrown exception, so the agent can self-correct:
121123

122124
```json
123125
{
124126
"error": "missing_xai_api_key",
125-
"message": "code_execution needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
127+
"message": "code_execution needs an xAI API key. Run openclaw onboard --auth-choice xai-api-key, set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
126128
"docs": "https://docs.openclaw.ai/tools/code-execution"
127129
}
128130
```

docs/tools/grok-search.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ OpenClaw supports Grok as a `web_search` provider, using xAI web-grounded
1010
responses to produce AI-synthesized answers backed by live search results
1111
with citations.
1212

13-
The same `XAI_API_KEY` can also power the built-in `x_search` tool for X
14-
(formerly Twitter) post search. If you store the key under
15-
`plugins.entries.xai.config.webSearch.apiKey`, OpenClaw now reuses it as a
16-
fallback for the bundled xAI model provider too.
13+
The same xAI API key can also power the built-in `x_search` tool for X
14+
(formerly Twitter) post search and the `code_execution` tool. If you store the
15+
key under `plugins.entries.xai.config.webSearch.apiKey`, OpenClaw now reuses it
16+
as a fallback for the bundled xAI model provider too.
1717

1818
For post-level X metrics such as reposts, replies, bookmarks, or views, prefer
1919
`x_search` with the exact post URL or status ID instead of a broad search

docs/tools/web.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ When you choose **Kimi** during `openclaw onboard` or
255255
- the default Kimi web-search model (defaults to `kimi-k2.6`)
256256

257257
For `x_search`, configure `plugins.entries.xai.config.xSearch.*`. It uses the
258-
same `XAI_API_KEY` fallback as Grok web search.
258+
same xAI auth profile as chat, or the `XAI_API_KEY` / plugin web-search
259+
credential used by Grok web search.
259260
Legacy `tools.web.x_search.*` config is auto-migrated by `openclaw doctor --fix`.
260261
When you choose Grok during `openclaw onboard` or `openclaw configure --section web`,
261262
OpenClaw can also offer optional `x_search` setup with the same key.
@@ -367,7 +368,7 @@ tool on the request that serves this tool call.
367368
cacheTtlMinutes: 15,
368369
},
369370
webSearch: {
370-
apiKey: "xai-...", // optional if XAI_API_KEY is set
371+
apiKey: "xai-...", // optional if an xAI auth profile or XAI_API_KEY is set
371372
baseUrl: "https://api.x.ai/v1", // optional shared xAI Responses base URL
372373
},
373374
},

extensions/xai/code-execution.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ describe("xai code_execution tool", () => {
6565
expect(tool?.name).toBe("code_execution");
6666
});
6767

68+
it("enables code_execution from an xAI auth profile and uses it for requests", async () => {
69+
const mockFetch = installCodeExecutionFetch();
70+
const tool = createCodeExecutionTool({
71+
config: {},
72+
auth: {
73+
hasAuthForProvider: (providerId) => providerId === "xai",
74+
resolveApiKeyForProvider: async (providerId) =>
75+
providerId === "xai" ? "xai-profile-key" : undefined, // pragma: allowlist secret
76+
},
77+
});
78+
79+
expect(tool?.name).toBe("code_execution");
80+
await tool?.execute?.("code-execution:auth-profile", {
81+
task: "Sum [20, 22]",
82+
});
83+
84+
const request = mockFetch.mock.calls[0]?.[1] as RequestInit | undefined;
85+
expect((request?.headers as Record<string, string> | undefined)?.Authorization).toBe(
86+
"Bearer xai-profile-key",
87+
);
88+
});
89+
6890
it("uses the xAI Responses code_interpreter tool", async () => {
6991
const mockFetch = installCodeExecutionFetch();
7092
const tool = createCodeExecutionTool({

extensions/xai/code-execution.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import {
77
resolveXaiCodeExecutionMaxTurns,
88
resolveXaiCodeExecutionModel,
99
} from "./src/code-execution-shared.js";
10-
import { isXaiToolEnabled, resolveXaiToolApiKey } from "./src/tool-auth-shared.js";
10+
import {
11+
isXaiToolEnabled,
12+
resolveXaiToolApiKeyWithAuth,
13+
type XaiToolAuthContext,
14+
} from "./src/tool-auth-shared.js";
1115

1216
type CodeExecutionConfig = {
1317
enabled?: boolean;
@@ -53,17 +57,20 @@ function resolveCodeExecutionEnabled(params: {
5357
sourceConfig?: unknown;
5458
runtimeConfig?: unknown;
5559
config?: CodeExecutionConfig;
60+
auth?: XaiToolAuthContext;
5661
}): boolean {
5762
return isXaiToolEnabled({
5863
enabled: readCodeExecutionConfigRecord(params.config)?.enabled as boolean | undefined,
5964
runtimeConfig: params.runtimeConfig as never,
6065
sourceConfig: params.sourceConfig as never,
66+
auth: params.auth,
6167
});
6268
}
6369

6470
export function createCodeExecutionTool(options?: {
6571
config?: unknown;
6672
runtimeConfig?: Record<string, unknown> | null;
73+
auth?: XaiToolAuthContext;
6774
}) {
6875
const runtimeConfig = options?.runtimeConfig ?? getRuntimeConfigSnapshot();
6976
const codeExecutionConfig =
@@ -74,6 +81,7 @@ export function createCodeExecutionTool(options?: {
7481
sourceConfig: options?.config,
7582
runtimeConfig: runtimeConfig ?? undefined,
7683
config: codeExecutionConfig,
84+
auth: options?.auth,
7785
})
7886
) {
7987
return null;
@@ -91,15 +99,16 @@ export function createCodeExecutionTool(options?: {
9199
}),
92100
}),
93101
execute: async (_toolCallId: string, args: Record<string, unknown>) => {
94-
const apiKey = resolveXaiToolApiKey({
102+
const apiKey = await resolveXaiToolApiKeyWithAuth({
95103
runtimeConfig: (runtimeConfig ?? undefined) as never,
96104
sourceConfig: options?.config as never,
105+
auth: options?.auth,
97106
});
98107
if (!apiKey) {
99108
return jsonResult({
100109
error: "missing_xai_api_key",
101110
message:
102-
"code_execution needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
111+
"code_execution needs an xAI API key. Run openclaw onboard --auth-choice xai-api-key, set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
103112
docs: "https://docs.openclaw.ai/tools/code-execution",
104113
});
105114
}

extensions/xai/index.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
22
import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared";
33
import { defaultToolStreamExtraParams } from "openclaw/plugin-sdk/provider-stream-shared";
4-
import { jsonResult, readProviderEnvValue } from "openclaw/plugin-sdk/provider-web-search";
4+
import { jsonResult } from "openclaw/plugin-sdk/provider-web-search";
55
import { Type } from "typebox";
66
import {
77
applyXaiRuntimeModelCompat,
@@ -16,7 +16,11 @@ import { buildXaiProvider } from "./provider-catalog.js";
1616
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
1717
import { buildXaiRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
1818
import { buildXaiSpeechProvider } from "./speech-provider.js";
19-
import { resolveFallbackXaiAuth } from "./src/tool-auth-shared.js";
19+
import {
20+
isXaiToolEnabled,
21+
resolveFallbackXaiAuth,
22+
type XaiToolAuthContext,
23+
} from "./src/tool-auth-shared.js";
2024
import { resolveEffectiveXSearchConfig } from "./src/x-search-config.js";
2125
import { wrapXaiProviderStream } from "./stream.js";
2226
import { buildXaiMediaUnderstandingProvider } from "./stt.js";
@@ -44,15 +48,13 @@ function loadXSearchModule(): Promise<XSearchModule> {
4448
return xSearchModulePromise;
4549
}
4650

47-
function hasResolvableXaiApiKey(config: unknown): boolean {
48-
return Boolean(
49-
resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]),
50-
);
51+
function hasResolvableXaiApiKey(config: unknown, auth?: XaiToolAuthContext): boolean {
52+
return isXaiToolEnabled({ sourceConfig: config as never, auth });
5153
}
5254

53-
function isCodeExecutionEnabled(config: unknown): boolean {
55+
function isCodeExecutionEnabled(config: unknown, auth?: XaiToolAuthContext): boolean {
5456
if (!config || typeof config !== "object") {
55-
return hasResolvableXaiApiKey(config);
57+
return hasResolvableXaiApiKey(config, auth);
5658
}
5759
const entries = (config as Record<string, unknown>).plugins;
5860
const pluginEntries =
@@ -74,26 +76,28 @@ function isCodeExecutionEnabled(config: unknown): boolean {
7476
if (codeExecution?.enabled === false) {
7577
return false;
7678
}
77-
return hasResolvableXaiApiKey(config);
79+
return hasResolvableXaiApiKey(config, auth);
7880
}
7981

80-
function isXSearchEnabled(config: unknown): boolean {
82+
function isXSearchEnabled(config: unknown, auth?: XaiToolAuthContext): boolean {
8183
const resolved =
8284
config && typeof config === "object"
8385
? resolveEffectiveXSearchConfig(config as never)
8486
: undefined;
8587
if (resolved?.enabled === false) {
8688
return false;
8789
}
88-
return hasResolvableXaiApiKey(config);
90+
return hasResolvableXaiApiKey(config, auth);
8991
}
9092

9193
function createLazyCodeExecutionTool(ctx: {
9294
config?: Record<string, unknown>;
9395
runtimeConfig?: Record<string, unknown>;
96+
hasAuthForProvider?: XaiToolAuthContext["hasAuthForProvider"];
97+
resolveApiKeyForProvider?: XaiToolAuthContext["resolveApiKeyForProvider"];
9498
}) {
9599
const effectiveConfig = ctx.runtimeConfig ?? ctx.config;
96-
if (!isCodeExecutionEnabled(effectiveConfig)) {
100+
if (!isCodeExecutionEnabled(effectiveConfig, ctx)) {
97101
return null;
98102
}
99103

@@ -113,12 +117,13 @@ function createLazyCodeExecutionTool(ctx: {
113117
const tool = createCodeExecutionTool({
114118
config: ctx.config as never,
115119
runtimeConfig: (ctx.runtimeConfig as never) ?? null,
120+
auth: ctx,
116121
});
117122
if (!tool) {
118123
return jsonResult({
119124
error: "missing_xai_api_key",
120125
message:
121-
"code_execution needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
126+
"code_execution needs an xAI API key. Run openclaw onboard --auth-choice xai-api-key, set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
122127
docs: "https://docs.openclaw.ai/tools/code-execution",
123128
});
124129
}
@@ -130,9 +135,11 @@ function createLazyCodeExecutionTool(ctx: {
130135
function createLazyXSearchTool(ctx: {
131136
config?: Record<string, unknown>;
132137
runtimeConfig?: Record<string, unknown>;
138+
hasAuthForProvider?: XaiToolAuthContext["hasAuthForProvider"];
139+
resolveApiKeyForProvider?: XaiToolAuthContext["resolveApiKeyForProvider"];
133140
}) {
134141
const effectiveConfig = ctx.runtimeConfig ?? ctx.config;
135-
if (!isXSearchEnabled(effectiveConfig)) {
142+
if (!isXSearchEnabled(effectiveConfig, ctx)) {
136143
return null;
137144
}
138145

@@ -141,6 +148,7 @@ function createLazyXSearchTool(ctx: {
141148
const tool = createXSearchTool({
142149
config: ctx.config as never,
143150
runtimeConfig: (ctx.runtimeConfig as never) ?? null,
151+
auth: ctx,
144152
});
145153
if (!tool) {
146154
return jsonResult(buildMissingXSearchApiKeyPayload());

extensions/xai/src/tool-auth-shared.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
resolveFallbackXaiAuth,
66
resolveFallbackXaiApiKey,
77
resolveXaiToolApiKey,
8+
resolveXaiToolApiKeyWithAuth,
89
} from "./tool-auth-shared.js";
910

1011
describe("xai tool auth helpers", () => {
@@ -138,6 +139,17 @@ describe("xai tool auth helpers", () => {
138139
expect(isXaiToolEnabled({ enabled: true })).toBe(true);
139140
});
140141

142+
it("uses xAI auth profiles when tool config and env are absent", async () => {
143+
const auth = {
144+
hasAuthForProvider: (providerId: string) => providerId === "xai",
145+
resolveApiKeyForProvider: async (providerId: string) =>
146+
providerId === "xai" ? "profile-key" : undefined, // pragma: allowlist secret
147+
};
148+
149+
expect(isXaiToolEnabled({ auth })).toBe(true);
150+
await expect(resolveXaiToolApiKeyWithAuth({ auth })).resolves.toBe("profile-key");
151+
});
152+
141153
it("does not use env fallback when a non-env SecretRef is configured but unavailable", () => {
142154
vi.stubEnv("XAI_API_KEY", "env-key");
143155

@@ -164,6 +176,34 @@ describe("xai tool auth helpers", () => {
164176
).toBeUndefined();
165177
});
166178

179+
it("does not bypass blocked explicit tool config with auth profiles", async () => {
180+
const auth = {
181+
hasAuthForProvider: (providerId: string) => providerId === "xai",
182+
resolveApiKeyForProvider: async () => "profile-key", // pragma: allowlist secret
183+
};
184+
185+
const sourceConfig = {
186+
plugins: {
187+
entries: {
188+
xai: {
189+
config: {
190+
webSearch: {
191+
apiKey: {
192+
source: "file",
193+
provider: "vault",
194+
id: "/xai/tool-key",
195+
},
196+
},
197+
},
198+
},
199+
},
200+
},
201+
};
202+
203+
expect(isXaiToolEnabled({ sourceConfig, auth })).toBe(false);
204+
await expect(resolveXaiToolApiKeyWithAuth({ sourceConfig, auth })).resolves.toBeUndefined();
205+
});
206+
167207
it("resolves env SecretRefs from source config when runtime snapshot is unavailable", () => {
168208
vi.stubEnv("XAI_API_KEY", "xai-secretref-key");
169209

0 commit comments

Comments
 (0)