mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
34a1cfc69c
* fix(cli-runner): drop stock watchdog defaults from CLI backend descriptors (#125045) The claude-cli and gemini-cli backend descriptors spread CLI_FRESH/RESUME_WATCHDOG_DEFAULTS into config.reliability.watchdog. Those copies are byte-identical to the fallback pickWatchdogProfile already uses, so shipping them only makes `configured` always truthy, which permanently disables the promotion gate (!configured) and pins resumed cron/explicit-timeout turns to the 180s resume no-output ceiling instead of the 600s fresh ceiling. Removing the blocks lets `configured` be undefined, restoring the promotion gate. No user-config path is affected: reliability.watchdog is not exposed in the config schema, the descriptor is the sole source, and pickWatchdogProfile is the only runtime reader. The codex backend never shipped the block, so this aligns the three backends. Co-Authored-By: Claude <noreply@anthropic.com> * test(cli-runner): add descriptor-through-resolver watchdog proof (#125045) Drive the real shipped Anthropic/Gemini CLI backend descriptor config through the production resolveCliNoOutputTimeoutMs resolver to prove the resumed-cron promotion is live post-fix. Pre-fix the descriptor shipped byte-identical resume defaults, so configured was truthy and the !configured promotion gate was dead, pinning resumed cron turns to the 180s resume ceiling (180000). Post-fix the block is gone and the resolver returns the promoted fresh budget (480000 = 600000 * 0.8). resolveCliNoOutputTimeoutMs is re-exported through the test-only openclaw/plugin-sdk/test-fixtures subpath (excluded from published dist), so extension tests can drive the real descriptor through the real resolver without crossing the extension/core boundary. Co-Authored-By: Claude <noreply@anthropic.com> * test(anthropic): split watchdog promotion proof into its own file Move the descriptor-shape and descriptor→resolver real-behavior proof tests out of cli-shared.test.ts into cli-watchdog-promotion.test.ts. cli-shared.test.ts exceeded the extensions max-lines budget (1000 non-blank/non-comment lines) once the proof tests landed; the focused file keeps the same test names and coverage while staying under the budget. No production change. Co-Authored-By: Claude <noreply@anthropic.com> * fix(cli-runner): restore inherited resume watchdog promotion Validate bundled plugin descriptors through their public setup entries and keep custom watchdog contracts intact. Co-authored-by: ruel225 <ruel225@users.noreply.github.com> * chore(cli-runner): align conflicting provider owner with main Co-authored-by: ruel225 <ruel225@users.noreply.github.com> * fix(cli-runner): complete inherited resume watchdog promotion Co-authored-by: ruel225 <ruel225@users.noreply.github.com> * test(cli): keep watchdog regressions in their owning plugins Keep Anthropic and Google descriptor assertions inside their plugin-owned registration suites, preserve generic resumed/custom watchdog coverage in its core owner, and remove the cross-plugin core test rejected by architecture CI. Co-authored-by: ruel225 <ruel225@users.noreply.github.com> --------- Co-authored-by: ruel225 <ruel225@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
import crypto from "node:crypto";
|
|
import type { CliBackendPlugin } from "openclaw/plugin-sdk/cli-backend";
|
|
const GEMINI_MODEL_ALIASES: Record<string, string> = {
|
|
pro: "gemini-3.1-pro-preview",
|
|
flash: "gemini-3.1-flash-preview",
|
|
"flash-lite": "gemini-3.1-flash-lite",
|
|
};
|
|
const GEMINI_CLI_DEFAULT_MODEL_REF = "google-gemini-cli/gemini-3-flash-preview";
|
|
const GEMINI_ALLOWED_MCP_SERVERS_ARG = "--allowed-mcp-server-names";
|
|
|
|
type GeminiCliBackendConfig = CliBackendPlugin["config"];
|
|
type GeminiCliOutputMode = NonNullable<GeminiCliBackendConfig["output"]>;
|
|
|
|
function mapGeminiCliOutputFormat(value: string | undefined): GeminiCliOutputMode | undefined {
|
|
if (value === "stream-json") {
|
|
return "jsonl";
|
|
}
|
|
if (value === "json" || value === "text") {
|
|
return value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function readGeminiCliOutputFormat(args: readonly string[] | undefined): GeminiCliOutputMode {
|
|
for (let index = 0; index < (args?.length ?? 0); index += 1) {
|
|
const arg = args?.[index];
|
|
if (arg === "--output-format" || arg === "-o") {
|
|
return mapGeminiCliOutputFormat(args?.[index + 1]) ?? "text";
|
|
}
|
|
const inline = arg?.startsWith("--output-format=")
|
|
? arg.slice("--output-format=".length)
|
|
: arg?.startsWith("-o=")
|
|
? arg.slice("-o=".length)
|
|
: undefined;
|
|
const mapped = mapGeminiCliOutputFormat(inline);
|
|
if (mapped) {
|
|
return mapped;
|
|
}
|
|
}
|
|
return "text";
|
|
}
|
|
|
|
function normalizeGeminiCliBackendConfig(config: GeminiCliBackendConfig): GeminiCliBackendConfig {
|
|
const output = readGeminiCliOutputFormat(config.args);
|
|
const resumeOutput = readGeminiCliOutputFormat(config.resumeArgs ?? config.args);
|
|
const usesStreamJson = output === "jsonl" || resumeOutput === "jsonl";
|
|
return {
|
|
...config,
|
|
output,
|
|
resumeOutput,
|
|
jsonlDialect: usesStreamJson ? "gemini-stream-json" : undefined,
|
|
};
|
|
}
|
|
|
|
function isGeminiAllowedMcpServersArg(arg: string): boolean {
|
|
const [name] = arg.split("=", 1);
|
|
if (!name?.startsWith("--")) {
|
|
return false;
|
|
}
|
|
return name.slice(2).replaceAll(/[-_]/g, "").toLowerCase() === "allowedmcpservernames";
|
|
}
|
|
|
|
function resolveGeminiCliExecutionArgs(
|
|
ctx: Parameters<NonNullable<CliBackendPlugin["resolveExecutionArgs"]>>[0],
|
|
): readonly string[] {
|
|
if (!ctx.toolAvailability) {
|
|
return ctx.baseArgs;
|
|
}
|
|
const terminatorIndex = ctx.baseArgs.indexOf("--");
|
|
const optionArgs = terminatorIndex === -1 ? ctx.baseArgs : ctx.baseArgs.slice(0, terminatorIndex);
|
|
const positionalArgs = terminatorIndex === -1 ? [] : ctx.baseArgs.slice(terminatorIndex);
|
|
const args: string[] = [];
|
|
for (let index = 0; index < optionArgs.length; index += 1) {
|
|
const arg = optionArgs[index];
|
|
if (arg && isGeminiAllowedMcpServersArg(arg)) {
|
|
if (!arg.includes("=")) {
|
|
index += 1;
|
|
}
|
|
continue;
|
|
}
|
|
if (arg !== undefined) {
|
|
args.push(arg);
|
|
}
|
|
}
|
|
|
|
// Gemini intersects file-based allowlists, where an empty intersection means
|
|
// unrestricted. The argv override bypasses that merge and prevents MCP startup.
|
|
const allowedServer = ctx.toolAvailability.openClaw.length > 0 ? "openclaw" : crypto.randomUUID();
|
|
return [...args, GEMINI_ALLOWED_MCP_SERVERS_ARG, allowedServer, ...positionalArgs];
|
|
}
|
|
|
|
export function buildGoogleGeminiCliBackend(): CliBackendPlugin {
|
|
return {
|
|
id: "google-gemini-cli",
|
|
modelProvider: "google",
|
|
liveTest: {
|
|
defaultModelRef: GEMINI_CLI_DEFAULT_MODEL_REF,
|
|
defaultImageProbe: true,
|
|
defaultMcpProbe: true,
|
|
docker: {
|
|
npmPackage: "@google/gemini-cli",
|
|
binaryName: "gemini",
|
|
},
|
|
},
|
|
// Gemini's published bundle owns inference; optional keychain/PTY modules
|
|
// are auth and tool integrations, not the inference transport.
|
|
runtimeArtifact: {
|
|
kind: "bundled-package-tree",
|
|
packageName: "@google/gemini-cli",
|
|
entrypoint: "command",
|
|
exactToolAvailabilityVersionPolicy: {
|
|
stableMinimum: "0.39.1",
|
|
prereleaseMinimums: {
|
|
preview: "0.40.0-preview.3",
|
|
nightly: "0.41.0-nightly.20260427.g42587de73",
|
|
},
|
|
},
|
|
},
|
|
bundleMcp: true,
|
|
bundleMcpMode: "gemini-system-settings",
|
|
nativeToolMode: "selectable",
|
|
toolAvailabilityEnforcement: "prepare-execution",
|
|
authEpochMode: "profile-only",
|
|
normalizeConfig: normalizeGeminiCliBackendConfig,
|
|
resolveExecutionArgs: resolveGeminiCliExecutionArgs,
|
|
prepareExecution: async (ctx) => {
|
|
const { prepareGeminiCliExecution } = await import("./cli-backend-auth.runtime.js");
|
|
const privateContext = ctx as typeof ctx & {
|
|
authCredential?: unknown;
|
|
isolatedCompletionCwd?: string;
|
|
isolatedCompletionModelId?: string;
|
|
isolatedCompletionPrompt?: string;
|
|
isolatedCompletionSystemPrompt?: string;
|
|
};
|
|
return await prepareGeminiCliExecution(
|
|
{
|
|
agentDir: ctx.agentDir,
|
|
authProfileId: ctx.authProfileId,
|
|
workspaceDir: ctx.workspaceDir,
|
|
baseEnv: ctx.env,
|
|
isolatedCompletionCwd: privateContext.isolatedCompletionCwd,
|
|
systemSettingsPath:
|
|
ctx.env?.GEMINI_CLI_SYSTEM_SETTINGS_PATH ?? process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH,
|
|
toolAvailability: ctx.toolAvailability,
|
|
// Gemini owns a native per-process system-prompt file. Consume the private
|
|
// core bridge without making isolated completion a public CLI SDK contract.
|
|
isolatedCompletionModelId: privateContext.isolatedCompletionModelId,
|
|
isolatedCompletionPrompt: privateContext.isolatedCompletionPrompt,
|
|
isolatedCompletionSystemPrompt: privateContext.isolatedCompletionSystemPrompt,
|
|
},
|
|
privateContext.authCredential,
|
|
);
|
|
},
|
|
config: {
|
|
command: "gemini",
|
|
args: [
|
|
"--skip-trust",
|
|
"--approval-mode",
|
|
"auto_edit",
|
|
"--output-format",
|
|
"stream-json",
|
|
"--prompt",
|
|
"{prompt}",
|
|
],
|
|
resumeArgs: [
|
|
"--skip-trust",
|
|
"--approval-mode",
|
|
"auto_edit",
|
|
"--resume",
|
|
"{sessionId}",
|
|
"--output-format",
|
|
"stream-json",
|
|
"--prompt",
|
|
"{prompt}",
|
|
],
|
|
output: "jsonl",
|
|
input: "arg",
|
|
jsonlDialect: "gemini-stream-json",
|
|
imageArg: "@",
|
|
imagePathScope: "workspace",
|
|
modelArg: "--model",
|
|
modelAliases: GEMINI_MODEL_ALIASES,
|
|
sessionMode: "existing",
|
|
sessionIdFields: ["session_id", "sessionId"],
|
|
serialize: true,
|
|
},
|
|
};
|
|
}
|