Resolve Gemini readiness from its selected native auth mode

Respect settings precedence and workspace trust when checking OAuth or API key credentials; leave other modes unverified.

Checks: 107 Gemini and readiness tests; desktop packaging; git diff --check.
This commit is contained in:
Ryder Sun committed 2026-11-07 20:09:20 +08:00
1 parent 7cdc88c4d1
commit 0f2babbcf8
3 files changed
+134

No files matched your search

@@ -0,0 +1,70 @@
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const JSON5 = require('json5')
function readSettings(file) {
try {
const stat = fs.statSync(file)
if (!stat.isFile() || stat.size > 1024 * 1024) throw new Error('invalid settings')
const value = JSON5.parse(fs.readFileSync(file, 'utf8'))
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid settings')
return value
} catch (error) {
if (error.code === 'ENOENT') return {}
throw error
}
}
function geminiNativeAuth(options = {}) {
const env = options.env || process.env
const platform = options.platform || process.platform
const home = options.home || os.homedir()
const base = env.GEMINI_CLI_HOME || home
const system = env.GEMINI_CLI_SYSTEM_SETTINGS_PATH || (platform === 'darwin'
? '/Library/Application Support/GeminiCli/settings.json' : platform === 'win32'
? 'C:\\ProgramData\\gemini-cli\\settings.json' : '/etc/gemini-cli/settings.json')
const defaults = env.GEMINI_CLI_SYSTEM_DEFAULTS_PATH || path.join(path.dirname(system), 'system-defaults.json')
if (![base, system, defaults].every(value => typeof value === 'string' && path.isAbsolute(value))) return { uncertain: true }
const root = path.join(base, '.gemini')
try {
const layers = [readSettings(defaults), readSettings(path.join(root, 'settings.json'))]
const systemSettings = readSettings(system)
const validOverrides = ['oauth-personal', 'gemini-api-key', 'vertex-ai', 'compute-default-credentials']
const override = validOverrides.includes(env.GEMINI_CLI_AUTH_OVERRIDE) ? env.GEMINI_CLI_AUTH_OVERRIDE : ''
let authType = override || (layers[1].security?.auth?.selectedType ?? layers[0].security?.auth?.selectedType ?? '')
const workdir = typeof options.workdir === 'string' && path.isAbsolute(options.workdir) ? options.workdir : ''
if (workdir && path.resolve(workdir) !== path.resolve(base)) {
let trusted = env.GEMINI_CLI_TRUST_WORKSPACE === 'true'
if (env.GEMINI_RESTRICTED_MODE === 'true' || env.GEMINI_CLI_TRUST_WORKSPACE === 'false') trusted = false
else if (!trusted) {
const enabled = systemSettings.security?.folderTrust?.enabled
?? layers[1].security?.folderTrust?.enabled ?? layers[0].security?.folderTrust?.enabled ?? true
if (enabled === false) trusted = true
else {
const trustFile = env.GEMINI_CLI_TRUSTED_FOLDERS_PATH || path.join(root, 'trustedFolders.json')
if (!path.isAbsolute(trustFile)) return { uncertain: true }
const rules = readSettings(trustFile)
const real = fs.realpathSync(workdir)
let longest = -1
for (const [folder, level] of Object.entries(rules)) {
if (!path.isAbsolute(folder) || !['TRUST_FOLDER', 'TRUST_PARENT', 'DO_NOT_TRUST'].includes(level)) continue
const candidate = level === 'TRUST_PARENT' ? path.dirname(folder) : folder
const rootPath = fs.existsSync(candidate) ? fs.realpathSync(candidate) : path.resolve(candidate)
const relative = path.relative(rootPath, real)
if ((relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)))
&& rootPath.length > longest) {
longest = rootPath.length
trusted = level !== 'DO_NOT_TRUST'
}
}
}
}
if (trusted) authType = readSettings(path.join(workdir, '.gemini', 'settings.json')).security?.auth?.selectedType ?? authType
}
authType = systemSettings.security?.auth?.selectedType ?? authType
return { authType: typeof authType === 'string' ? authType : '', root }
} catch { return { uncertain: true } }
}
module.exports = { geminiNativeAuth }
@@ -12,6 +12,7 @@ const { OCR_PROVIDER_KEYS } = require('./cli/ocr-provider-environment.cjs')
const { deepseekNativeConfig } = require('./cli/deepseek-native-config.cjs')
const { hermesProviderFromConfig, hermesProviderEnvironment } = require('./hermes-config-summary.cjs')
const { qwenCredentialSelection, qwenConfiguredAuth } = require('./cli/qwen-native-config.cjs')
const { geminiNativeAuth } = require('./cli/gemini-native-config.cjs')
const { prepareCommand, searchPath } = require('./cli/cli-discovery.cjs')
const { isOpenClawSecretReference } = require('./cli/openclaw-runtime.cjs')
const { NETWORK_ENV_KEYS, networkEnvironment } = require('./cli/cli-network-environment.cjs')
@@ -803,6 +804,21 @@ function nativeCredentialState(kind, options = {}) {
const geminiHome = env.GEMINI_CLI_HOME
? (path.isAbsolute(env.GEMINI_CLI_HOME) ? path.join(env.GEMINI_CLI_HOME, '.gemini') : null)
: path.join(home, '.gemini')
if (kind === 'gemini') {
const selection = geminiNativeAuth({ home, env, workdir: options.workdir, platform: options.platform })
if (selection.uncertain) return { state: 'unknown', source: 'unverified' }
if (selection.authType === 'oauth-personal') {
return jsonContainsCredential(path.join(selection.root, 'oauth_creds.json'))
? { state: 'ready', source: 'native-credential' }
: { state: 'unknown', source: 'unverified' }
}
if (selection.authType === 'gemini-api-key') {
const found = ['GOOGLE_API_KEY', 'GEMINI_API_KEY'].some(key => typeof env[key] === 'string' && env[key].trim())
return found ? { state: 'ready', source: options.credentialSource === 'native-shell' ? 'native-shell' : 'native-credential' }
: { state: 'unknown', source: 'unverified' }
}
if (selection.authType) return { state: 'unknown', source: 'unverified' }
}
const expandHome = value => String(value).replace(/^~(?=$|[\\/])/, home)
const piHome = env.PI_CODING_AGENT_DIR ? expandHome(env.PI_CODING_AGENT_DIR) : path.join(home, '.pi', 'agent')
const kimiHome = env.KIMI_CODE_HOME
@@ -0,0 +1,48 @@
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { geminiNativeAuth } = require('../../../src/agents/cli/gemini-native-config.cjs')
const { nativeCredentialState } = require('../../../src/agents/local-agent-readiness.cjs')
test('Gemini resolves auth through native settings and workspace trust', t => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-gemini-auth-'))
t.after(() => fs.rmSync(home, { recursive: true, force: true }))
const root = path.join(home, '.gemini')
const project = path.join(home, 'project')
fs.mkdirSync(root)
fs.mkdirSync(path.join(project, '.gemini'), { recursive: true })
const defaults = path.join(home, 'defaults.json')
const system = path.join(home, 'system.json')
const env = { GEMINI_CLI_SYSTEM_SETTINGS_PATH: system, GEMINI_CLI_SYSTEM_DEFAULTS_PATH: defaults }
const write = (file, auth) => fs.writeFileSync(file, JSON.stringify({ security: { auth: { selectedType: auth } } }))
write(defaults, 'oauth-personal')
write(path.join(root, 'settings.json'), 'gemini-api-key')
write(path.join(project, '.gemini', 'settings.json'), 'vertex-ai')
assert.equal(geminiNativeAuth({ home, env, workdir: project }).authType, 'gemini-api-key')
fs.writeFileSync(path.join(root, 'trustedFolders.json'), JSON.stringify({ [project]: 'TRUST_FOLDER' }))
assert.equal(geminiNativeAuth({ home, env, workdir: project }).authType, 'vertex-ai')
assert.equal(geminiNativeAuth({ home, env: { ...env, GEMINI_RESTRICTED_MODE: 'true' }, workdir: project }).authType, 'gemini-api-key')
assert.equal(geminiNativeAuth({ home, env: { ...env, GEMINI_CLI_AUTH_OVERRIDE: 'oauth-personal' } }).authType, 'oauth-personal')
write(system, 'vertex-ai')
assert.equal(geminiNativeAuth({ home, env: { ...env, GEMINI_CLI_AUTH_OVERRIDE: 'oauth-personal' }, workdir: project }).authType, 'vertex-ai')
})
test('Gemini readiness checks credentials for its selected auth mode', t => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'meldwork-gemini-ready-'))
t.after(() => fs.rmSync(home, { recursive: true, force: true }))
const root = path.join(home, '.gemini')
fs.mkdirSync(root)
const env = { GEMINI_CLI_SYSTEM_SETTINGS_PATH: path.join(home, 'system.json'), GEMINI_CLI_AUTH_OVERRIDE: 'oauth-personal', GEMINI_API_KEY: 'unrelated' }
assert.deepEqual(nativeCredentialState('gemini', { home, env }), { state: 'unknown', source: 'unverified' })
fs.writeFileSync(path.join(root, 'oauth_creds.json'), JSON.stringify({ refresh_token: 'oauth-token' }))
assert.deepEqual(nativeCredentialState('gemini', { home, env }), { state: 'ready', source: 'native-credential' })
env.GEMINI_CLI_AUTH_OVERRIDE = 'gemini-api-key'
assert.deepEqual(nativeCredentialState('gemini', { home, env }), { state: 'ready', source: 'native-credential' })
env.GEMINI_CLI_AUTH_OVERRIDE = 'vertex-ai'
assert.deepEqual(nativeCredentialState('gemini', { home, env }), { state: 'unknown', source: 'unverified' })
env.GEMINI_CLI_AUTH_OVERRIDE = 'gemini-api-key'
delete env.GEMINI_API_KEY
assert.deepEqual(nativeCredentialState('gemini', { home, env }), { state: 'unknown', source: 'unverified' })
})