mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 10:59:49 +00:00
OAuth login flow
----------------
- Hard-require refresh_token after the authorization-code exchange in
xai-oauth.ts. Access-only responses persisted credentials that the
downstream usability check later rejected; the new requireRefreshToken
option fails the exchange instead. Error wording explains the missing
refresh_token in OIDC scope terms (offline_access scope rejected),
not a "grant".
- Derive token expiry from the access-token JWT exp claim when
expires_in is missing. id_token exp is intentionally not used as a
fallback because id_token lifetime tracks the OIDC session, not the
access token, and would defer refresh past actual expiry.
- Handle CORS preflight OPTIONS on the loopback OAuth callback in
src/plugin-sdk/provider-auth-runtime.ts. The previous handler treated
any non-callback request as a failed GET, returned "Missing code or
state", and tore the server down before the real GET arrived. The
CORS allowlist is now an optional `corsOriginAllowlist` parameter on
waitForLocalOAuthCallback so the SDK helper stays generic. The xAI
plugin passes ["auth.x.ai", "accounts.x.ai"] from loginXaiOAuth.
Sidecar surfaces
----------------
- speech-provider.ts (POST /v1/tts) honors the xAI OAuth profile in
addition to provider config and XAI_API_KEY. isConfigured now also
reports true when an xAI auth profile is configured (via
isProviderAuthProfileConfigured), so OAuth-only users are no longer
silently filtered out by the selection layer. The bearer resolver
threads req.cfg into resolveApiKeyForProvider so the right xAI auth
profile is picked when a user has multiple.
- realtime-transcription-provider.ts (WSS /stt) gets the same
isConfigured fix, and the lazy headers() resolver threads req.cfg
into the OAuth bearer lookup. createSession stays sync per its
plugin contract.
- stt.ts: drop the plugin-side OAuth fallback. The media-understanding
core already resolves auth (cfg/agentDir-aware) via
resolveProviderExecutionContext before calling transcribeAudio, so
the wrapper was redundant. transcribeAudio is now the registered
hook directly.
User-Agent attribution
----------------------
- New buildXaiAttributionPolicy in src/agents/provider-attribution.ts
injects User-Agent: openclaw/<version>, originator, and version on
/v1/responses and /v1/chat/completions traffic that goes through
resolveProviderRequestHeaders. Gated to xai-native and default
endpoint classes; custom proxy baseUrls remain withheld. reviewNote
is honest about which headers are spec-verified vs mirrored.
- Shared extensions/xai/src/xai-user-agent.ts helper exports
xaiUserAgentHeaderFor(baseUrl) which only emits the User-Agent when
the resolved baseUrl points at the xAI-native API host. Threaded
through TTS and realtime STT (WS upgrade headers) so user-configured
proxy baseUrls do not receive the openclaw identity. OAuth discovery
and token endpoints still send User-Agent unconditionally because
isTrustedXaiOAuthEndpoint already restricts those URLs to *.x.ai.
- Image gen, batch STT, and video gen rely on the attribution policy
alone (no manual User-Agent in defaultHeaders), so attribution
withholding on user-configured proxy baseUrls is preserved
end-to-end.
- UA is bearer-agnostic: same value whether the bearer comes from an
xAI API key or the xAI OAuth flow.
Drop dead api.grok.x.ai alias
-----------------------------
- xAI retired the api.grok.x.ai alias; DNS now returns NXDOMAIN from
xAI's own authoritative nameservers. Drop it from the xai-native
endpoint host set in extensions/xai/openclaw.plugin.json,
extensions/xai/api.ts, extensions/xai/tts.ts, and the
openai-responses payload policy. Update the attribution test to
classify api.grok.x.ai as "custom" (no live user can reach it; the
classification keeps documenting the host's status).
Video generation now matches xAI's actual API behavior
------------------------------------------------------
Previously, real video generation requests failed with
"xAI video generation response malformed" because the poll-status
handler validated against a closed enum that did not match what the
xAI service actually returns. Four fixes:
- Loosen the poll-status handler. xAI returns intermediate strings
outside `["queued", "processing", "done", "failed", "expired"]`
(commonly `submitted`, `pending`, `in_progress`, ...). Treat `done`
as terminal-success, `["failed", "error", "expired", "cancelled"]`
as terminal-failure, and any other string (including empty) as
continue-polling. Also accept `cancelled` as a terminal failure.
- Send default duration/aspect_ratio/resolution on every generate and
reference-image submit. xAI rejects bodies that omit these fields.
Defaults: duration=8s, aspect_ratio="16:9", resolution="720p".
- Accept lowercase resolution input ("480p"/"720p"/"1080p") in
addition to uppercase, normalize to lowercase on the wire.
- Add an `x-idempotency-key` header (fresh `crypto.randomUUID()`) on
every submit so a network retry does not double-charge the user.
Polls intentionally reuse the unmodified `headers` without the key.
Ergonomics
----------
- All "missing xAI credentials" errors (code_execution, lazy
code_execution fallback in extensions/xai/index.ts, x_search,
web_search grok in web-search-provider.runtime.ts, TTS, batch STT,
realtime STT) now mention `openclaw onboard --auth-choice xai-oauth`
first.
- Dedupe the Grok model-id alias table: model-compat.ts re-exports
normalizeXaiModelId from model-id.ts as normalizeNativeXaiModelId.
Test coverage
-------------
- src/plugin-sdk/provider-auth-runtime.test.ts: locks the new pure
buildOAuthCallbackOriginResolver gate (allowlist match,
case-normalization, https-only, non-allowlisted hosts dropped,
multi-Origin handling).
- extensions/xai/xai-oauth.test.ts: locks
XAI_OAUTH_CALLBACK_CORS_ORIGIN_ALLOWLIST so loginXaiOAuth keeps
threading the right hosts to the SDK helper.
- extensions/xai/speech-provider.test.ts: OAuth-only auth profile
flips isConfigured to true; cfg threads into the OAuth fallback
resolver.
- extensions/xai/realtime-transcription-provider.test.ts: same +
upgrade headers carry the OAuth bearer end-to-end.
- extensions/xai/stt.test.ts: explicit assertion that transcribeAudio
trusts the core-resolved apiKey (no plugin-side wrapper).
Verification
------------
- pnpm install: clean
- 154/154 vitest tests pass across 13 touched test files
- pnpm check:changed: typecheck core/ext + tests, oxlint core/ext,
runtime guards, dependency pin guard, package patch guard, runtime
import cycles, sidecar loader guard - all green
- pnpm build: 0 errors, 0 [INEFFECTIVE_DYNAMIC_IMPORT] warnings
147 lines
5.0 KiB
TypeScript
147 lines
5.0 KiB
TypeScript
import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search";
|
|
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
import { Type } from "typebox";
|
|
import {
|
|
buildXaiCodeExecutionPayload,
|
|
requestXaiCodeExecution,
|
|
resolveXaiCodeExecutionMaxTurns,
|
|
resolveXaiCodeExecutionModel,
|
|
} from "./src/code-execution-shared.js";
|
|
import {
|
|
isXaiToolEnabled,
|
|
resolveXaiToolApiKeyWithAuth,
|
|
type XaiToolAuthContext,
|
|
} from "./src/tool-auth-shared.js";
|
|
|
|
type CodeExecutionConfig = {
|
|
enabled?: boolean;
|
|
model?: string;
|
|
maxTurns?: number;
|
|
timeoutSeconds?: number;
|
|
};
|
|
|
|
function readCodeExecutionConfigRecord(
|
|
config?: CodeExecutionConfig,
|
|
): Record<string, unknown> | undefined {
|
|
return config && typeof config === "object" ? (config as Record<string, unknown>) : undefined;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
export function createCodeExecutionTool(options?: {
|
|
config?: unknown;
|
|
runtimeConfig?: Record<string, unknown> | null;
|
|
auth?: XaiToolAuthContext;
|
|
}) {
|
|
const runtimeConfig = options?.runtimeConfig ?? getRuntimeConfigSnapshot();
|
|
const codeExecutionConfig =
|
|
readPluginCodeExecutionConfig(runtimeConfig ?? undefined) ??
|
|
readPluginCodeExecutionConfig(options?.config);
|
|
if (
|
|
!resolveCodeExecutionEnabled({
|
|
sourceConfig: options?.config,
|
|
runtimeConfig: runtimeConfig ?? undefined,
|
|
config: codeExecutionConfig,
|
|
auth: options?.auth,
|
|
})
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
label: "Code Execution",
|
|
name: "code_execution",
|
|
description:
|
|
"Run sandboxed Python analysis with xAI. Use for calculations, tabulation, summaries, and chart-style analysis without local machine access.",
|
|
parameters: Type.Object({
|
|
task: Type.String({
|
|
description:
|
|
"The full analysis task for xAI's remote Python sandbox. Include any data to analyze directly in the task.",
|
|
}),
|
|
}),
|
|
execute: async (_toolCallId: string, args: Record<string, unknown>) => {
|
|
const apiKey = await resolveXaiToolApiKeyWithAuth({
|
|
runtimeConfig: (runtimeConfig ?? undefined) as never,
|
|
sourceConfig: options?.config as never,
|
|
auth: options?.auth,
|
|
});
|
|
if (!apiKey) {
|
|
return jsonResult({
|
|
error: "missing_xai_api_key",
|
|
message:
|
|
"code_execution 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`.",
|
|
docs: "https://docs.openclaw.ai/tools/code-execution",
|
|
});
|
|
}
|
|
|
|
const task = readStringParam(args, "task", { required: true });
|
|
const codeExecutionConfigRecord = readCodeExecutionConfigRecord(codeExecutionConfig);
|
|
const model = resolveXaiCodeExecutionModel(codeExecutionConfigRecord);
|
|
const maxTurns = resolveXaiCodeExecutionMaxTurns(codeExecutionConfigRecord);
|
|
const timeoutSeconds =
|
|
typeof codeExecutionConfigRecord?.timeoutSeconds === "number" &&
|
|
Number.isFinite(codeExecutionConfigRecord.timeoutSeconds)
|
|
? codeExecutionConfigRecord.timeoutSeconds
|
|
: 30;
|
|
const startedAt = Date.now();
|
|
const result = await requestXaiCodeExecution({
|
|
apiKey,
|
|
model,
|
|
timeoutSeconds,
|
|
maxTurns,
|
|
task,
|
|
});
|
|
return jsonResult(
|
|
buildXaiCodeExecutionPayload({
|
|
task,
|
|
model,
|
|
tookMs: Date.now() - startedAt,
|
|
content: result.content,
|
|
citations: result.citations,
|
|
usedCodeExecution: result.usedCodeExecution,
|
|
outputTypes: result.outputTypes,
|
|
}),
|
|
);
|
|
},
|
|
};
|
|
}
|