fix: persist Gemini CLI auth homes

This commit is contained in:
Shakker
2026-06-15 18:54:37 +01:00
committed by Shakker
parent 94a4a3fbc4
commit 6afb08f9ab
4 changed files with 272 additions and 89 deletions

View File

@@ -1,3 +1,4 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import type { CliBackendPlugin } from "openclaw/plugin-sdk/cli-backend";
@@ -5,7 +6,6 @@ import {
CLI_FRESH_WATCHDOG_DEFAULTS,
CLI_RESUME_WATCHDOG_DEFAULTS,
} from "openclaw/plugin-sdk/cli-backend";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
const GEMINI_MODEL_ALIASES: Record<string, string> = {
pro: "gemini-3.1-pro-preview",
@@ -15,10 +15,12 @@ const GEMINI_MODEL_ALIASES: Record<string, string> = {
const GEMINI_CLI_DEFAULT_MODEL_REF = "google-gemini-cli/gemini-3-flash-preview";
const GEMINI_CLI_PROVIDER_ID = "google-gemini-cli";
const VERCEL_AI_GATEWAY_PROVIDER_ID = "vercel-ai-gateway";
const GEMINI_CLI_CREDENTIALS_FILENAME = "gemini-credentials.json";
const GEMINI_CLI_GCA_AUTH_ENV = [
"GOOGLE_GENAI_USE_GCA",
"GOOGLE_CLOUD_ACCESS_TOKEN",
"GEMINI_FORCE_ENCRYPTED_FILE_STORAGE",
"GEMINI_FORCE_FILE_STORAGE",
];
const GEMINI_CLI_API_KEY_AUTH_ENV = [
...GEMINI_CLI_GCA_AUTH_ENV,
@@ -31,11 +33,11 @@ const GEMINI_CLI_API_KEY_AUTH_ENV = [
"GEMINI_CLI_CUSTOM_HEADERS",
"GEMINI_API_KEY_AUTH_MECHANISM",
];
const GEMINI_CLI_PROFILE_AUTH_ENV = [...GEMINI_CLI_API_KEY_AUTH_ENV, "GEMINI_API_KEY"];
type PreparedGeminiCliExecution = {
env: Record<string, string>;
clearEnv: string[];
cleanup: () => Promise<void>;
};
function normalizeString(value: string | undefined): string | undefined {
@@ -68,6 +70,11 @@ type GeminiApiKeyCredential = GeminiAuthProfileCredential & {
key: string;
};
type GeminiCliAuthHomeContext = {
agentDir?: string;
authProfileId?: string;
};
function throwUnsupportedGeminiCredential(credential: GeminiAuthProfileCredential): never {
if (credential.provider === VERCEL_AI_GATEWAY_PROVIDER_ID) {
throw new Error(
@@ -140,25 +147,58 @@ function requireGeminiApiKeyCredential(
};
}
async function createIsolatedGeminiCliHome(settings: unknown): Promise<{
tempHome: string;
function resolveGeminiCliProfileHome(ctx: GeminiCliAuthHomeContext): {
home: string;
geminiDir: string;
} {
const agentDir = normalizeString(ctx.agentDir);
if (!agentDir) {
throw new Error("Gemini CLI auth profile execution requires an agent directory.");
}
const authProfileId = normalizeString(ctx.authProfileId);
if (!authProfileId) {
throw new Error("Gemini CLI auth profile execution requires a selected auth profile.");
}
const profileHash = crypto.createHash("sha256").update(authProfileId).digest("hex").slice(0, 24);
const home = path.join(agentDir, "cli-runtimes", GEMINI_CLI_PROVIDER_ID, "profiles", profileHash);
return { home, geminiDir: path.join(home, ".gemini") };
}
async function writeGeminiCliJson(filePath: string, value: unknown): Promise<void> {
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, {
encoding: "utf8",
mode: 0o600,
});
await fs.chmod(filePath, 0o600);
}
async function prepareGeminiCliProfileHome(
ctx: GeminiCliAuthHomeContext,
settings: unknown,
): Promise<{
home: string;
geminiDir: string;
}> {
const tempHome = await fs.mkdtemp(
path.join(resolvePreferredOpenClawTmpDir(), "gemini-cli-home-"),
);
await fs.chmod(tempHome, 0o700);
const geminiDir = path.join(tempHome, ".gemini");
const { home, geminiDir } = resolveGeminiCliProfileHome(ctx);
await fs.mkdir(geminiDir, { recursive: true, mode: 0o700 });
await fs.writeFile(
path.join(geminiDir, "settings.json"),
`${JSON.stringify(settings, null, 2)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
return { tempHome, geminiDir };
await fs.chmod(home, 0o700);
await fs.chmod(geminiDir, 0o700);
await Promise.all([
writeGeminiCliJson(path.join(geminiDir, "settings.json"), settings),
writeGeminiCliJson(path.join(home, "settings.json"), settings),
]);
return { home, geminiDir };
}
async function clearGeminiCliCachedCredentials(geminiDir: string): Promise<void> {
// Gemini prefers its token store over oauth_creds.json. Rebuild that store
// from the selected OpenClaw profile each run so stale CLI auth cannot win.
await fs.rm(path.join(geminiDir, GEMINI_CLI_CREDENTIALS_FILENAME), { force: true });
}
async function prepareGeminiCliOAuthHome(
ctx: GeminiCliAuthHomeContext,
credential: GeminiAuthProfileCredential | undefined,
): Promise<PreparedGeminiCliExecution | null> {
const oauth = requireGeminiOAuthCredential(credential);
@@ -166,43 +206,34 @@ async function prepareGeminiCliOAuthHome(
return null;
}
const { tempHome, geminiDir } = await createIsolatedGeminiCliHome({
const { home, geminiDir } = await prepareGeminiCliProfileHome(ctx, {
security: { auth: { selectedType: "oauth-personal" } },
});
try {
const idToken = normalizeString(oauth.idToken);
const oauthCreds: Record<string, string | number> = {
access_token: oauth.access,
refresh_token: oauth.refresh,
expiry_date: oauth.expires,
token_type: "Bearer",
};
if (idToken) {
oauthCreds.id_token = idToken;
}
await fs.writeFile(
path.join(geminiDir, "oauth_creds.json"),
`${JSON.stringify(oauthCreds, null, 2)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
return {
env: {
GEMINI_CLI_HOME: tempHome,
},
clearEnv: [...GEMINI_CLI_GCA_AUTH_ENV],
cleanup: async () => {
await fs.rm(tempHome, { recursive: true, force: true });
},
};
} catch (error) {
await fs.rm(tempHome, { recursive: true, force: true });
throw error;
await clearGeminiCliCachedCredentials(geminiDir);
const idToken = normalizeString(oauth.idToken);
const oauthCreds: Record<string, string | number> = {
access_token: oauth.access,
refresh_token: oauth.refresh,
expiry_date: oauth.expires,
token_type: "Bearer",
};
if (idToken) {
oauthCreds.id_token = idToken;
}
await writeGeminiCliJson(path.join(geminiDir, "oauth_creds.json"), oauthCreds);
return {
env: {
GEMINI_CLI_HOME: home,
GEMINI_FORCE_FILE_STORAGE: "true",
},
clearEnv: [...GEMINI_CLI_PROFILE_AUTH_ENV],
};
}
async function prepareGeminiCliApiKeyHome(
ctx: GeminiCliAuthHomeContext,
credential: GeminiAuthProfileCredential | undefined,
): Promise<PreparedGeminiCliExecution | null> {
const apiKey = requireGeminiApiKeyCredential(credential);
@@ -210,31 +241,30 @@ async function prepareGeminiCliApiKeyHome(
return null;
}
const { tempHome } = await createIsolatedGeminiCliHome({
const { home, geminiDir } = await prepareGeminiCliProfileHome(ctx, {
security: { auth: { selectedType: "gemini-api-key" } },
});
try {
return {
env: {
GEMINI_CLI_HOME: tempHome,
GEMINI_API_KEY: apiKey.key,
},
clearEnv: [...GEMINI_CLI_API_KEY_AUTH_ENV],
cleanup: async () => {
await fs.rm(tempHome, { recursive: true, force: true });
},
};
} catch (error) {
await fs.rm(tempHome, { recursive: true, force: true });
throw error;
}
await Promise.all([
fs.rm(path.join(geminiDir, "oauth_creds.json"), { force: true }),
clearGeminiCliCachedCredentials(geminiDir),
]);
return {
env: {
GEMINI_CLI_HOME: home,
GEMINI_FORCE_FILE_STORAGE: "true",
GEMINI_API_KEY: apiKey.key,
},
clearEnv: [...GEMINI_CLI_PROFILE_AUTH_ENV],
};
}
async function prepareGeminiCliAuthHome(
ctx: GeminiCliAuthHomeContext,
credential: GeminiAuthProfileCredential | undefined,
): Promise<PreparedGeminiCliExecution | null> {
return (
(await prepareGeminiCliOAuthHome(credential)) ?? (await prepareGeminiCliApiKeyHome(credential))
(await prepareGeminiCliOAuthHome(ctx, credential)) ??
(await prepareGeminiCliApiKeyHome(ctx, credential))
);
}
@@ -257,6 +287,10 @@ export function buildGoogleGeminiCliBackend(): CliBackendPlugin {
authEpochMode: "profile-only",
prepareExecution: async (ctx) =>
await prepareGeminiCliAuthHome(
{
agentDir: ctx.agentDir,
authProfileId: ctx.authProfileId,
},
(ctx as typeof ctx & { authCredential?: GeminiAuthProfileCredential }).authCredential,
),
config: {

View File

@@ -23,8 +23,10 @@ type GeminiPrepareContext = Parameters<
};
function buildGeminiOAuthPrepareContext(workspaceDir: string): GeminiPrepareContext {
const agentDir = path.join(workspaceDir, "agent");
return {
workspaceDir,
agentDir,
provider: "google-gemini-cli",
modelId: "gemini-3.1-pro-preview",
authProfileId: "google-gemini-cli:user@example.test",
@@ -42,8 +44,10 @@ function buildGeminiOAuthPrepareContext(workspaceDir: string): GeminiPrepareCont
}
function buildGeminiApiKeyPrepareContext(workspaceDir: string): GeminiPrepareContext {
const agentDir = path.join(workspaceDir, "agent");
return {
workspaceDir,
agentDir,
provider: "google-gemini-cli",
modelId: "gemini-3.1-flash-lite",
authProfileId: "google-gemini-cli:api-key",
@@ -85,20 +89,20 @@ describe("google setup entry", () => {
});
describe("google gemini cli backend auth bridge", () => {
it("materializes selected OpenClaw OAuth credentials into an isolated Gemini CLI home", async () => {
it("materializes selected OpenClaw OAuth credentials into a persistent profile-scoped Gemini CLI home", async () => {
const backend = buildGoogleGeminiCliBackend();
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));
let prepared:
| Awaited<ReturnType<NonNullable<typeof backend.prepareExecution>>>
| null
| undefined;
let home: string | undefined;
try {
prepared = await backend.prepareExecution?.(buildGeminiOAuthPrepareContext(workspaceDir));
const context = buildGeminiOAuthPrepareContext(workspaceDir);
const prepared = await backend.prepareExecution?.(context);
home = prepared?.env?.GEMINI_CLI_HOME;
expect(home).toBeTruthy();
expect(prepared?.env?.GEMINI_FORCE_FILE_STORAGE).toBe("true");
expect(home).toContain(path.join(context.agentDir, "cli-runtimes", "google-gemini-cli"));
expect(home).not.toContain("user@example.test");
const raw = await fs.readFile(path.join(home ?? "", ".gemini", "oauth_creds.json"), "utf8");
expect(JSON.parse(raw)).toEqual({
@@ -108,29 +112,54 @@ describe("google gemini cli backend auth bridge", () => {
expiry_date: 1_800_000_000_000,
token_type: "Bearer",
});
const nestedSettingsRaw = await fs.readFile(
path.join(home ?? "", ".gemini", "settings.json"),
"utf8",
);
const rootSettingsRaw = await fs.readFile(path.join(home ?? "", "settings.json"), "utf8");
expect(JSON.parse(nestedSettingsRaw)).toEqual({
security: { auth: { selectedType: "oauth-personal" } },
});
expect(JSON.parse(rootSettingsRaw)).toEqual(JSON.parse(nestedSettingsRaw));
const sessionMarker = path.join(home ?? "", ".gemini", "session-state.json");
await fs.writeFile(sessionMarker, '{"keep":true}\n', "utf8");
const cachedCredentialsPath = path.join(home ?? "", ".gemini", "gemini-credentials.json");
await fs.writeFile(cachedCredentialsPath, "stale-cache", "utf8");
const preparedAgain = await backend.prepareExecution?.(context);
expect(preparedAgain?.env?.GEMINI_CLI_HOME).toBe(home);
await expect(fs.access(sessionMarker)).resolves.toBeUndefined();
await expect(fs.access(cachedCredentialsPath)).rejects.toThrow();
} finally {
await prepared?.cleanup?.();
await fs.rm(workspaceDir, { recursive: true, force: true });
}
await expect(fs.access(home ?? "")).rejects.toThrow();
});
it("prepares selected Gemini API-key credentials without writing OAuth state", async () => {
it("prepares selected Gemini API-key credentials and removes stale OAuth state for that profile home", async () => {
const backend = buildGoogleGeminiCliBackend();
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));
let prepared:
| Awaited<ReturnType<NonNullable<typeof backend.prepareExecution>>>
| null
| undefined;
let home: string | undefined;
try {
prepared = await backend.prepareExecution?.(buildGeminiApiKeyPrepareContext(workspaceDir));
const context = buildGeminiApiKeyPrepareContext(workspaceDir);
const firstPrepared = await backend.prepareExecution?.(context);
home = firstPrepared?.env?.GEMINI_CLI_HOME;
expect(home).toBeTruthy();
await fs.writeFile(path.join(home ?? "", ".gemini", "oauth_creds.json"), "{}\n", "utf8");
await fs.writeFile(
path.join(home ?? "", ".gemini", "gemini-credentials.json"),
"stale-cache",
"utf8",
);
const prepared = await backend.prepareExecution?.(context);
home = prepared?.env?.GEMINI_CLI_HOME;
expect(home).toBeTruthy();
expect(prepared?.env?.GEMINI_API_KEY).toBe("gemini-api-key");
expect(prepared?.env?.GEMINI_FORCE_FILE_STORAGE).toBe("true");
expect(prepared?.clearEnv).toContain("GEMINI_API_KEY");
expect(prepared?.clearEnv).toContain("GOOGLE_GENAI_USE_GCA");
expect(prepared?.clearEnv).toContain("GOOGLE_GENAI_USE_VERTEXAI");
expect(prepared?.clearEnv).toContain("GOOGLE_GEMINI_BASE_URL");
@@ -145,12 +174,12 @@ describe("google gemini cli backend auth bridge", () => {
await expect(
fs.access(path.join(home ?? "", ".gemini", "oauth_creds.json")),
).rejects.toThrow();
await expect(
fs.access(path.join(home ?? "", ".gemini", "gemini-credentials.json")),
).rejects.toThrow();
} finally {
await prepared?.cleanup?.();
await fs.rm(workspaceDir, { recursive: true, force: true });
}
await expect(fs.access(home ?? "")).rejects.toThrow();
});
it("rejects Vercel AI Gateway profiles for the Gemini CLI backend", async () => {
@@ -161,6 +190,7 @@ describe("google gemini cli backend auth bridge", () => {
await expect(
backend.prepareExecution?.({
workspaceDir,
agentDir: path.join(workspaceDir, "agent"),
provider: "google-gemini-cli",
modelId: "gemini-3.1-flash-lite",
authProfileId: "vercel-ai-gateway:default",
@@ -176,12 +206,14 @@ describe("google gemini cli backend auth bridge", () => {
}
});
it("clears inherited Gemini GCA credentials when staging selected OAuth credentials", async () => {
it("clears inherited Gemini auth credentials when staging selected OAuth credentials", async () => {
const backend = buildGoogleGeminiCliBackend();
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));
const originalUseGca = process.env.GOOGLE_GENAI_USE_GCA;
const originalCloudAccessToken = process.env.GOOGLE_CLOUD_ACCESS_TOKEN;
const originalForceEncryptedFileStorage = process.env.GEMINI_FORCE_ENCRYPTED_FILE_STORAGE;
const originalGeminiApiKey = process.env.GEMINI_API_KEY;
const originalGoogleApiKey = process.env.GOOGLE_API_KEY;
let prepared:
| Awaited<ReturnType<NonNullable<typeof backend.prepareExecution>>>
| null
@@ -190,6 +222,8 @@ describe("google gemini cli backend auth bridge", () => {
process.env.GOOGLE_GENAI_USE_GCA = "true";
process.env.GOOGLE_CLOUD_ACCESS_TOKEN = "ambient-cloud-token";
process.env.GEMINI_FORCE_ENCRYPTED_FILE_STORAGE = "true";
process.env.GEMINI_API_KEY = "ambient-gemini-key";
process.env.GOOGLE_API_KEY = "ambient-google-key";
try {
prepared = await backend.prepareExecution?.(buildGeminiOAuthPrepareContext(workspaceDir));
@@ -199,16 +233,40 @@ describe("google gemini cli backend auth bridge", () => {
"GOOGLE_GENAI_USE_GCA",
"GOOGLE_CLOUD_ACCESS_TOKEN",
"GEMINI_FORCE_ENCRYPTED_FILE_STORAGE",
"GEMINI_FORCE_FILE_STORAGE",
"GOOGLE_GENAI_USE_VERTEXAI",
"GOOGLE_API_KEY",
"GOOGLE_CLOUD_PROJECT",
"GOOGLE_CLOUD_PROJECT_ID",
"GOOGLE_CLOUD_LOCATION",
"GOOGLE_GEMINI_BASE_URL",
"GEMINI_CLI_CUSTOM_HEADERS",
"GEMINI_API_KEY_AUTH_MECHANISM",
"GEMINI_API_KEY",
]);
} finally {
restoreEnv("GOOGLE_GENAI_USE_GCA", originalUseGca);
restoreEnv("GOOGLE_CLOUD_ACCESS_TOKEN", originalCloudAccessToken);
restoreEnv("GEMINI_FORCE_ENCRYPTED_FILE_STORAGE", originalForceEncryptedFileStorage);
restoreEnv("GEMINI_API_KEY", originalGeminiApiKey);
restoreEnv("GOOGLE_API_KEY", originalGoogleApiKey);
await prepared?.cleanup?.();
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("requires an agent directory for profile-owned Gemini CLI state", async () => {
const backend = buildGoogleGeminiCliBackend();
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));
try {
const { agentDir: _agentDir, ...context } = buildGeminiOAuthPrepareContext(workspaceDir);
await expect(backend.prepareExecution?.(context)).rejects.toThrow(/agent directory/);
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("uses profile-only auth epochs for the private Gemini CLI bridge", () => {
const backend = buildGoogleGeminiCliBackend();

View File

@@ -22,6 +22,8 @@ import {
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { captureEnv, setTestEnvValue } from "../../test-utils/env.js";
import { resolveApiKeyForProfile as resolveApiKeyForProfileImpl } from "../auth-profiles/oauth.js";
import { saveAuthProfileStore } from "../auth-profiles/store.js";
import { testing as cliBackendsTesting } from "../cli-backends.js";
import { hashCliSessionText } from "../cli-session.js";
import { resetContextWindowCacheForTest } from "../context.js";
@@ -263,6 +265,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
args: [],
cleanup: vi.fn(async () => undefined),
})),
resolveApiKeyForProfile: resolveApiKeyForProfileImpl,
});
mockGetGlobalHookRunner.mockReturnValue(null);
getRuntimeConfigMock.mockReturnValue({});
@@ -321,6 +324,92 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
).toBe(false);
});
it("passes raw refreshed OAuth profile fields to profile-owned CLI preparation", async () => {
const { dir, sessionFile } = createSessionFile();
const agentDir = path.join(dir, "agents", "main", "agent");
const authProfileId = "google-gemini-cli:user@example.test";
const prepareExecution = vi.fn(async () => ({
env: { GEMINI_CLI_HOME: path.join(agentDir, "gemini-home") },
}));
const resolveApiKeyForProfile = vi.fn(async () => ({
apiKey: JSON.stringify({ token: "provider-formatted-access", projectId: "project-1" }),
provider: "google-gemini-cli",
email: "user@example.test",
}));
fs.mkdirSync(agentDir, { recursive: true });
saveAuthProfileStore(
{
version: 1,
profiles: {
[authProfileId]: {
type: "oauth",
provider: "google-gemini-cli",
access: "raw-access-token",
refresh: "raw-refresh-token",
expires: 1_800_000_000_000,
projectId: "project-1",
email: "user@example.test",
},
},
},
agentDir,
);
cliBackendsTesting.setDepsForTest({
resolvePluginSetupCliBackend: () => undefined,
resolveRuntimeCliBackends: () => [
{
id: "google-gemini-cli",
pluginId: "google",
bundleMcp: false,
authEpochMode: "profile-only",
prepareExecution,
config: {
command: "gemini",
args: ["--prompt", "{prompt}"],
output: "json",
input: "arg",
sessionMode: "existing",
},
},
],
});
setCliRunnerPrepareTestDeps({
resolveApiKeyForProfile,
});
try {
await prepareCliRunContext({
sessionId: "session-test",
sessionKey: "agent:main:main",
sessionFile,
workspaceDir: dir,
prompt: "latest ask",
provider: "google-gemini-cli",
model: "gemini-3.1-pro-preview",
timeoutMs: 1_000,
runId: "run-test-gemini-oauth-raw-profile-fields",
authProfileId,
config: {},
});
expect(resolveApiKeyForProfile).toHaveBeenCalledOnce();
expect(prepareExecution).toHaveBeenCalledWith(
expect.objectContaining({
authProfileId,
authCredential: expect.objectContaining({
type: "oauth",
provider: "google-gemini-cli",
access: "raw-access-token",
refresh: "raw-refresh-token",
expires: 1_800_000_000_000,
}),
}),
);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it("prepares side questions without agent-turn context, tools, hooks, or reusable sessions", async () => {
const { dir, sessionFile } = createSessionFile();
appendTranscriptEntry(sessionFile, {

View File

@@ -106,6 +106,7 @@ const prepareDeps = {
prepareClaudeCliSkillsPlugin,
claudeCliSessionTranscriptHasContent,
claudeCliSessionTranscriptHasOrphanedToolUse,
resolveApiKeyForProfile,
};
async function resolveCliSkillsPrompt(params: {
@@ -314,7 +315,7 @@ export async function prepareCliRunContext(
profileId: authProfileId,
}),
});
const resolvedAuth = await resolveApiKeyForProfile({
const resolvedAuth = await prepareDeps.resolveApiKeyForProfile({
cfg: params.config,
store: writableAuthStore,
profileId: authProfileId,
@@ -329,12 +330,13 @@ export async function prepareCliRunContext(
});
authCredential = authStore.profiles[authProfileId];
if (resolvedAuth && authCredential) {
authCredential =
authCredential.type === "api_key"
? { ...authCredential, key: resolvedAuth.apiKey }
: authCredential.type === "token"
? { ...authCredential, token: resolvedAuth.apiKey }
: { ...authCredential, access: resolvedAuth.apiKey };
// Apply resolved strings only to static credentials with secret refs.
// OAuth CLI bridges need raw refreshed fields from the reloaded store.
if (authCredential.type === "api_key") {
authCredential = { ...authCredential, key: resolvedAuth.apiKey };
} else if (authCredential.type === "token") {
authCredential = { ...authCredential, token: resolvedAuth.apiKey };
}
}
}
const extraSystemPrompt = params.extraSystemPrompt?.trim() ?? "";