feat(adapters/gemini-local): add auth detection, turn-limit handling, sandbox, and approval modes
Incorporate improvements from PR #13 and #105 into the gemini-local adapter: - Add detectGeminiAuthRequired() for runtime auth failure detection with errorCode: "gemini_auth_required" on execution results - Add isGeminiTurnLimitResult() to detect exit code 53 / turn_limit status and clear session to prevent stuck sessions on next heartbeat - Add describeGeminiFailure() for structured error messages from parsed result events including errors array extraction - Return parsed resultEvent in resultJson instead of raw stdout/stderr - Add isRetry guard to prevent stale session ID fallback after retry - Replace boolean yolo with approvalMode string (default/auto_edit/yolo) with backwards-compatible config.yolo fallback - Add sandbox config option (--sandbox / --sandbox=none) - Add GOOGLE_GENAI_USE_GCA auth detection in environment test - Consolidate auth detection regex into shared detectGeminiAuthRequired() - Add gemini-2.0-flash and gemini-2.0-flash-lite model IDs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@ export const models = [
|
|||||||
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
||||||
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
|
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
|
||||||
{ id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" },
|
{ id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" },
|
||||||
|
{ id: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
|
||||||
|
{ id: "gemini-2.0-flash-lite", label: "Gemini 2.0 Flash Lite" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const agentConfigurationDoc = `# gemini_local agent configuration
|
export const agentConfigurationDoc = `# gemini_local agent configuration
|
||||||
@@ -28,7 +30,8 @@ Core fields:
|
|||||||
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt
|
||||||
- promptTemplate (string, optional): run prompt template
|
- promptTemplate (string, optional): run prompt template
|
||||||
- model (string, optional): Gemini model id. Defaults to auto.
|
- model (string, optional): Gemini model id. Defaults to auto.
|
||||||
- yolo (boolean, optional): pass --approval-mode yolo for unattended operation
|
- approvalMode (string, optional): "default", "auto_edit", or "yolo" (default: "default")
|
||||||
|
- sandbox (boolean, optional): run in sandbox mode (default: false, passes --sandbox=none)
|
||||||
- command (string, optional): defaults to "gemini"
|
- command (string, optional): defaults to "gemini"
|
||||||
- extraArgs (string[], optional): additional CLI args
|
- extraArgs (string[], optional): additional CLI args
|
||||||
- env (object, optional): KEY=VALUE environment variables
|
- env (object, optional): KEY=VALUE environment variables
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ import {
|
|||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
||||||
import { isGeminiUnknownSessionError, parseGeminiJsonl } from "./parse.js";
|
import {
|
||||||
|
describeGeminiFailure,
|
||||||
|
detectGeminiAuthRequired,
|
||||||
|
isGeminiTurnLimitResult,
|
||||||
|
isGeminiUnknownSessionError,
|
||||||
|
parseGeminiJsonl,
|
||||||
|
} from "./parse.js";
|
||||||
import { firstNonEmptyLine } from "./utils.js";
|
import { firstNonEmptyLine } from "./utils.js";
|
||||||
|
|
||||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -93,7 +99,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
);
|
);
|
||||||
const command = asString(config.command, "gemini");
|
const command = asString(config.command, "gemini");
|
||||||
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
||||||
const yolo = asBoolean(config.yolo, false);
|
const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, false) ? "yolo" : "default");
|
||||||
|
const sandbox = asBoolean(config.sandbox, false);
|
||||||
|
|
||||||
const workspaceContext = parseObject(context.paperclipWorkspace);
|
const workspaceContext = parseObject(context.paperclipWorkspace);
|
||||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||||
@@ -211,7 +218,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
}
|
}
|
||||||
const commandNotes = (() => {
|
const commandNotes = (() => {
|
||||||
const notes: string[] = ["Prompt is passed to Gemini as the final positional argument."];
|
const notes: string[] = ["Prompt is passed to Gemini as the final positional argument."];
|
||||||
if (yolo) notes.push("Added --approval-mode yolo for unattended execution.");
|
if (approvalMode !== "default") notes.push(`Added --approval-mode ${approvalMode} for unattended execution.`);
|
||||||
if (!instructionsFilePath) return notes;
|
if (!instructionsFilePath) return notes;
|
||||||
if (instructionsPrefix.length > 0) {
|
if (instructionsPrefix.length > 0) {
|
||||||
notes.push(
|
notes.push(
|
||||||
@@ -242,7 +249,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
const args = ["--output-format", "stream-json"];
|
const args = ["--output-format", "stream-json"];
|
||||||
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
||||||
if (model && model !== DEFAULT_GEMINI_LOCAL_MODEL) args.push("--model", model);
|
if (model && model !== DEFAULT_GEMINI_LOCAL_MODEL) args.push("--model", model);
|
||||||
if (yolo) args.push("--approval-mode", "yolo");
|
if (approvalMode !== "default") args.push("--approval-mode", approvalMode);
|
||||||
|
if (sandbox) {
|
||||||
|
args.push("--sandbox");
|
||||||
|
} else {
|
||||||
|
args.push("--sandbox=none");
|
||||||
|
}
|
||||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||||
args.push(prompt);
|
args.push(prompt);
|
||||||
return args;
|
return args;
|
||||||
@@ -290,18 +302,31 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
parsed: ReturnType<typeof parseGeminiJsonl>;
|
parsed: ReturnType<typeof parseGeminiJsonl>;
|
||||||
},
|
},
|
||||||
clearSessionOnMissingSession = false,
|
clearSessionOnMissingSession = false,
|
||||||
|
isRetry = false,
|
||||||
): AdapterExecutionResult => {
|
): AdapterExecutionResult => {
|
||||||
|
const authMeta = detectGeminiAuthRequired({
|
||||||
|
parsed: attempt.parsed.resultEvent,
|
||||||
|
stdout: attempt.proc.stdout,
|
||||||
|
stderr: attempt.proc.stderr,
|
||||||
|
});
|
||||||
|
|
||||||
if (attempt.proc.timedOut) {
|
if (attempt.proc.timedOut) {
|
||||||
return {
|
return {
|
||||||
exitCode: attempt.proc.exitCode,
|
exitCode: attempt.proc.exitCode,
|
||||||
signal: attempt.proc.signal,
|
signal: attempt.proc.signal,
|
||||||
timedOut: true,
|
timedOut: true,
|
||||||
errorMessage: `Timed out after ${timeoutSec}s`,
|
errorMessage: `Timed out after ${timeoutSec}s`,
|
||||||
|
errorCode: authMeta.requiresAuth ? "gemini_auth_required" : null,
|
||||||
clearSession: clearSessionOnMissingSession,
|
clearSession: clearSessionOnMissingSession,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedSessionId = attempt.parsed.sessionId ?? runtimeSessionId ?? runtime.sessionId ?? null;
|
const clearSessionForTurnLimit = isGeminiTurnLimitResult(attempt.parsed.resultEvent, attempt.proc.exitCode);
|
||||||
|
|
||||||
|
// On retry, don't fall back to old session ID — the old session was stale
|
||||||
|
const canFallbackToRuntimeSession = !isRetry;
|
||||||
|
const resolvedSessionId = attempt.parsed.sessionId
|
||||||
|
?? (canFallbackToRuntimeSession ? (runtimeSessionId ?? runtime.sessionId ?? null) : null);
|
||||||
const resolvedSessionParams = resolvedSessionId
|
const resolvedSessionParams = resolvedSessionId
|
||||||
? ({
|
? ({
|
||||||
sessionId: resolvedSessionId,
|
sessionId: resolvedSessionId,
|
||||||
@@ -313,8 +338,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
: null;
|
: null;
|
||||||
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
||||||
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
||||||
|
const structuredFailure = attempt.parsed.resultEvent
|
||||||
|
? describeGeminiFailure(attempt.parsed.resultEvent)
|
||||||
|
: null;
|
||||||
const fallbackErrorMessage =
|
const fallbackErrorMessage =
|
||||||
parsedError ||
|
parsedError ||
|
||||||
|
structuredFailure ||
|
||||||
stderrLine ||
|
stderrLine ||
|
||||||
`Gemini exited with code ${attempt.proc.exitCode ?? -1}`;
|
`Gemini exited with code ${attempt.proc.exitCode ?? -1}`;
|
||||||
|
|
||||||
@@ -323,6 +352,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
signal: attempt.proc.signal,
|
signal: attempt.proc.signal,
|
||||||
timedOut: false,
|
timedOut: false,
|
||||||
errorMessage: (attempt.proc.exitCode ?? 0) === 0 ? null : fallbackErrorMessage,
|
errorMessage: (attempt.proc.exitCode ?? 0) === 0 ? null : fallbackErrorMessage,
|
||||||
|
errorCode: authMeta.requiresAuth ? "gemini_auth_required" : null,
|
||||||
usage: attempt.parsed.usage,
|
usage: attempt.parsed.usage,
|
||||||
sessionId: resolvedSessionId,
|
sessionId: resolvedSessionId,
|
||||||
sessionParams: resolvedSessionParams,
|
sessionParams: resolvedSessionParams,
|
||||||
@@ -331,12 +361,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
model,
|
model,
|
||||||
billingType,
|
billingType,
|
||||||
costUsd: attempt.parsed.costUsd,
|
costUsd: attempt.parsed.costUsd,
|
||||||
resultJson: {
|
resultJson: attempt.parsed.resultEvent ?? {
|
||||||
stdout: attempt.proc.stdout,
|
stdout: attempt.proc.stdout,
|
||||||
stderr: attempt.proc.stderr,
|
stderr: attempt.proc.stderr,
|
||||||
},
|
},
|
||||||
summary: attempt.parsed.summary,
|
summary: attempt.parsed.summary,
|
||||||
clearSession: Boolean(clearSessionOnMissingSession && !resolvedSessionId),
|
clearSession: clearSessionForTurnLimit || Boolean(clearSessionOnMissingSession && !resolvedSessionId),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -353,7 +383,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
`[paperclip] Gemini resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
|
`[paperclip] Gemini resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`,
|
||||||
);
|
);
|
||||||
const retry = await runAttempt(null);
|
const retry = await runAttempt(null);
|
||||||
return toResult(retry, true);
|
return toResult(retry, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return toResult(initial);
|
return toResult(initial);
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
export { execute } from "./execute.js";
|
export { execute } from "./execute.js";
|
||||||
export { testEnvironment } from "./test.js";
|
export { testEnvironment } from "./test.js";
|
||||||
export { parseGeminiJsonl, isGeminiUnknownSessionError } from "./parse.js";
|
export {
|
||||||
|
parseGeminiJsonl,
|
||||||
|
isGeminiUnknownSessionError,
|
||||||
|
describeGeminiFailure,
|
||||||
|
detectGeminiAuthRequired,
|
||||||
|
isGeminiTurnLimitResult,
|
||||||
|
} from "./parse.js";
|
||||||
import type { AdapterSessionCodec } from "@paperclipai/adapter-utils";
|
import type { AdapterSessionCodec } from "@paperclipai/adapter-utils";
|
||||||
|
|
||||||
function readNonEmptyString(value: unknown): string | null {
|
function readNonEmptyString(value: unknown): string | null {
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export function parseGeminiJsonl(stdout: string) {
|
|||||||
const messages: string[] = [];
|
const messages: string[] = [];
|
||||||
let errorMessage: string | null = null;
|
let errorMessage: string | null = null;
|
||||||
let costUsd: number | null = null;
|
let costUsd: number | null = null;
|
||||||
|
let resultEvent: Record<string, unknown> | null = null;
|
||||||
const usage = {
|
const usage = {
|
||||||
inputTokens: 0,
|
inputTokens: 0,
|
||||||
cachedInputTokens: 0,
|
cachedInputTokens: 0,
|
||||||
@@ -101,6 +102,7 @@ export function parseGeminiJsonl(stdout: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === "result") {
|
if (type === "result") {
|
||||||
|
resultEvent = event;
|
||||||
accumulateUsage(usage, event.usage ?? event.usageMetadata);
|
accumulateUsage(usage, event.usage ?? event.usageMetadata);
|
||||||
const resultText =
|
const resultText =
|
||||||
asString(event.result, "").trim() ||
|
asString(event.result, "").trim() ||
|
||||||
@@ -151,6 +153,7 @@ export function parseGeminiJsonl(stdout: string) {
|
|||||||
usage,
|
usage,
|
||||||
costUsd,
|
costUsd,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
resultEvent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,3 +168,75 @@ export function isGeminiUnknownSessionError(stdout: string, stderr: string): boo
|
|||||||
haystack,
|
haystack,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractGeminiErrorMessages(parsed: Record<string, unknown>): string[] {
|
||||||
|
const messages: string[] = [];
|
||||||
|
const errorMsg = asString(parsed.error, "").trim();
|
||||||
|
if (errorMsg) messages.push(errorMsg);
|
||||||
|
|
||||||
|
const raw = Array.isArray(parsed.errors) ? parsed.errors : [];
|
||||||
|
for (const entry of raw) {
|
||||||
|
if (typeof entry === "string") {
|
||||||
|
const msg = entry.trim();
|
||||||
|
if (msg) messages.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
||||||
|
const obj = entry as Record<string, unknown>;
|
||||||
|
const msg = asString(obj.message, "") || asString(obj.error, "") || asString(obj.code, "");
|
||||||
|
if (msg) {
|
||||||
|
messages.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
messages.push(JSON.stringify(obj));
|
||||||
|
} catch {
|
||||||
|
// skip non-serializable entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function describeGeminiFailure(parsed: Record<string, unknown>): string | null {
|
||||||
|
const status = asString(parsed.status, "");
|
||||||
|
const errors = extractGeminiErrorMessages(parsed);
|
||||||
|
|
||||||
|
const detail = errors[0] ?? "";
|
||||||
|
const parts = ["Gemini run failed"];
|
||||||
|
if (status) parts.push(`status=${status}`);
|
||||||
|
if (detail) parts.push(detail);
|
||||||
|
return parts.length > 1 ? parts.join(": ") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GEMINI_AUTH_REQUIRED_RE = /(?:not\s+authenticated|please\s+authenticate|api[_ ]?key\s+(?:required|missing|invalid)|authentication\s+required|unauthorized|invalid\s+credentials|GEMINI_API_KEY|GOOGLE_API_KEY|not\s+logged\s+in|login\s+required|run\s+`?gemini\s+auth(?:\s+login)?`?\s+first)/i;
|
||||||
|
|
||||||
|
export function detectGeminiAuthRequired(input: {
|
||||||
|
parsed: Record<string, unknown> | null;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}): { requiresAuth: boolean } {
|
||||||
|
const errors = extractGeminiErrorMessages(input.parsed ?? {});
|
||||||
|
const messages = [...errors, input.stdout, input.stderr]
|
||||||
|
.join("\n")
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const requiresAuth = messages.some((line) => GEMINI_AUTH_REQUIRED_RE.test(line));
|
||||||
|
return { requiresAuth };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGeminiTurnLimitResult(
|
||||||
|
parsed: Record<string, unknown> | null | undefined,
|
||||||
|
exitCode?: number | null,
|
||||||
|
): boolean {
|
||||||
|
if (exitCode === 53) return true;
|
||||||
|
if (!parsed) return false;
|
||||||
|
|
||||||
|
const status = asString(parsed.status, "").trim().toLowerCase();
|
||||||
|
if (status === "turn_limit" || status === "max_turns") return true;
|
||||||
|
|
||||||
|
const error = asString(parsed.error, "").trim();
|
||||||
|
return /turn\s*limit|max(?:imum)?\s+turns?/i.test(error);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
|
||||||
import { parseGeminiJsonl } from "./parse.js";
|
import { detectGeminiAuthRequired, parseGeminiJsonl } from "./parse.js";
|
||||||
import { firstNonEmptyLine } from "./utils.js";
|
import { firstNonEmptyLine } from "./utils.js";
|
||||||
|
|
||||||
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
|
||||||
@@ -41,9 +41,6 @@ function summarizeProbeDetail(stdout: string, stderr: string, parsedError: strin
|
|||||||
return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean;
|
return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GEMINI_AUTH_REQUIRED_RE =
|
|
||||||
/(?:authentication\s+required|not\s+authenticated|not\s+logged\s+in|login\s+required|unauthorized|invalid(?:\s+or\s+missing)?\s+api(?:[_\s-]?key)?|gemini[_\s-]?api[_\s-]?key|google[_\s-]?api[_\s-]?key|run\s+`?gemini\s+auth(?:\s+login)?`?\s+first|api(?:[_\s-]?key)?(?:\s+is)?\s+required)/i;
|
|
||||||
|
|
||||||
export async function testEnvironment(
|
export async function testEnvironment(
|
||||||
ctx: AdapterEnvironmentTestContext,
|
ctx: AdapterEnvironmentTestContext,
|
||||||
): Promise<AdapterEnvironmentTestResult> {
|
): Promise<AdapterEnvironmentTestResult> {
|
||||||
@@ -94,15 +91,19 @@ export async function testEnvironment(
|
|||||||
const hostGeminiApiKey = process.env.GEMINI_API_KEY;
|
const hostGeminiApiKey = process.env.GEMINI_API_KEY;
|
||||||
const configGoogleApiKey = env.GOOGLE_API_KEY;
|
const configGoogleApiKey = env.GOOGLE_API_KEY;
|
||||||
const hostGoogleApiKey = process.env.GOOGLE_API_KEY;
|
const hostGoogleApiKey = process.env.GOOGLE_API_KEY;
|
||||||
|
const hasGca = env.GOOGLE_GENAI_USE_GCA === "true" || process.env.GOOGLE_GENAI_USE_GCA === "true";
|
||||||
if (
|
if (
|
||||||
isNonEmpty(configGeminiApiKey) ||
|
isNonEmpty(configGeminiApiKey) ||
|
||||||
isNonEmpty(hostGeminiApiKey) ||
|
isNonEmpty(hostGeminiApiKey) ||
|
||||||
isNonEmpty(configGoogleApiKey) ||
|
isNonEmpty(configGoogleApiKey) ||
|
||||||
isNonEmpty(hostGoogleApiKey)
|
isNonEmpty(hostGoogleApiKey) ||
|
||||||
|
hasGca
|
||||||
) {
|
) {
|
||||||
const source = isNonEmpty(configGeminiApiKey) || isNonEmpty(configGoogleApiKey)
|
const source = hasGca
|
||||||
? "adapter config env"
|
? "Google account login (GCA)"
|
||||||
: "server environment";
|
: isNonEmpty(configGeminiApiKey) || isNonEmpty(configGoogleApiKey)
|
||||||
|
? "adapter config env"
|
||||||
|
: "server environment";
|
||||||
checks.push({
|
checks.push({
|
||||||
code: "gemini_api_key_present",
|
code: "gemini_api_key_present",
|
||||||
level: "info",
|
level: "info",
|
||||||
@@ -114,7 +115,7 @@ export async function testEnvironment(
|
|||||||
code: "gemini_api_key_missing",
|
code: "gemini_api_key_missing",
|
||||||
level: "warn",
|
level: "warn",
|
||||||
message: "No Gemini API key was detected. Gemini runs may fail until auth is configured.",
|
message: "No Gemini API key was detected. Gemini runs may fail until auth is configured.",
|
||||||
hint: "Set GEMINI_API_KEY or GOOGLE_API_KEY in adapter env/shell, or run `gemini auth` / `gemini auth login`.",
|
hint: "Set GEMINI_API_KEY or GOOGLE_API_KEY in adapter env/shell, run `gemini auth` / `gemini auth login`, or set GOOGLE_GENAI_USE_GCA=true for Google account auth.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +159,11 @@ export async function testEnvironment(
|
|||||||
);
|
);
|
||||||
const parsed = parseGeminiJsonl(probe.stdout);
|
const parsed = parseGeminiJsonl(probe.stdout);
|
||||||
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
||||||
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
|
const authMeta = detectGeminiAuthRequired({
|
||||||
|
parsed: parsed.resultEvent,
|
||||||
|
stdout: probe.stdout,
|
||||||
|
stderr: probe.stderr,
|
||||||
|
});
|
||||||
|
|
||||||
if (probe.timedOut) {
|
if (probe.timedOut) {
|
||||||
checks.push({
|
checks.push({
|
||||||
@@ -183,7 +188,7 @@ export async function testEnvironment(
|
|||||||
hint: "Try `gemini --output-format json \"Respond with hello.\"` manually to inspect full output.",
|
hint: "Try `gemini --output-format json \"Respond with hello.\"` manually to inspect full output.",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} else if (GEMINI_AUTH_REQUIRED_RE.test(authEvidence)) {
|
} else if (authMeta.requiresAuth) {
|
||||||
checks.push({
|
checks.push({
|
||||||
code: "gemini_hello_probe_auth_required",
|
code: "gemini_hello_probe_auth_required",
|
||||||
level: "warn",
|
level: "warn",
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ export function buildGeminiLocalConfig(v: CreateConfigValues): Record<string, un
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Object.keys(env).length > 0) ac.env = env;
|
if (Object.keys(env).length > 0) ac.env = env;
|
||||||
if (v.dangerouslyBypassSandbox) ac.yolo = true;
|
if (v.dangerouslyBypassSandbox) ac.approvalMode = "yolo";
|
||||||
|
ac.sandbox = !v.dangerouslyBypassSandbox;
|
||||||
if (v.command) ac.command = v.command;
|
if (v.command) ac.command = v.command;
|
||||||
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
|
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);
|
||||||
return ac;
|
return ac;
|
||||||
|
|||||||
Reference in New Issue
Block a user