Files
openclaw/extensions/google/cli-backend.ts
ruel225 34a1cfc69c fix(cli-runner): drop stock watchdog defaults that disable resume promotion (#125045) (#125085)
* 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>
2026-08-25 04:14:45 -07:00

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,
},
};
}