2025-08-13 11:06:31 -07:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-08-25 22:11:27 +02:00
import * as fs from 'node:fs' ;
import * as path from 'node:path' ;
2026-02-09 09:16:56 -08:00
import * as crypto from 'node:crypto' ;
import { lock } from 'proper-lockfile' ;
2025-09-03 11:44:26 -07:00
import {
2025-09-22 17:06:43 -07:00
FatalConfigError ,
2025-09-03 11:44:26 -07:00
getErrorMessage ,
isWithinRoot ,
2025-09-11 13:07:57 -07:00
ideContextStore ,
2025-10-14 02:31:39 +09:00
GEMINI_DIR ,
2026-01-06 20:09:39 -08:00
homedir ,
2026-02-09 15:46:49 -08:00
isHeadlessMode ,
2026-02-09 09:16:56 -08:00
coreEvents ,
2026-02-11 16:20:54 -08:00
type HeadlessModeOptions ,
2025-09-03 11:44:26 -07:00
} from '@google/gemini-cli-core' ;
2025-08-26 00:04:53 +02:00
import type { Settings } from './settings.js' ;
2025-08-13 11:06:31 -07:00
import stripJsonComments from 'strip-json-comments' ;
2026-02-09 09:16:56 -08:00
const { promises : fsPromises } = fs ;
2025-08-13 11:06:31 -07:00
export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json' ;
2025-10-31 19:17:01 +00:00
export function getUserSettingsDir ( ) : string {
return path . join ( homedir ( ) , GEMINI_DIR ) ;
}
2025-09-16 09:58:57 -07:00
export function getTrustedFoldersPath ( ) : string {
if ( process . env [ 'GEMINI_CLI_TRUSTED_FOLDERS_PATH' ] ) {
return process . env [ 'GEMINI_CLI_TRUSTED_FOLDERS_PATH' ] ;
}
2025-10-31 19:17:01 +00:00
return path . join ( getUserSettingsDir ( ) , TRUSTED_FOLDERS_FILENAME ) ;
2025-09-16 09:58:57 -07:00
}
2025-08-13 11:06:31 -07:00
export enum TrustLevel {
TRUST_FOLDER = 'TRUST_FOLDER' ,
TRUST_PARENT = 'TRUST_PARENT' ,
DO_NOT_TRUST = 'DO_NOT_TRUST' ,
}
2026-02-03 14:53:31 -08:00
export function isTrustLevel (
value : string | number | boolean | object | null | undefined ,
) : value is TrustLevel {
2025-12-19 19:13:03 -05:00
return (
typeof value === 'string' &&
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-12-19 19:13:03 -05:00
Object . values ( TrustLevel ) . includes ( value as TrustLevel )
) ;
}
2025-08-13 11:06:31 -07:00
export interface TrustRule {
path : string ;
trustLevel : TrustLevel ;
}
export interface TrustedFoldersError {
message : string ;
path : string ;
}
export interface TrustedFoldersFile {
config : Record < string , TrustLevel > ;
path : string ;
}
2025-09-22 11:45:02 -07:00
export interface TrustResult {
isTrusted : boolean | undefined ;
source : 'ide' | 'file' | undefined ;
}
2026-02-03 14:53:31 -08:00
const realPathCache = new Map < string , string > ( ) ;
2026-02-09 09:16:56 -08:00
/**
* Parses the trusted folders JSON content, stripping comments.
*/
function parseTrustedFoldersJson ( content : string ) : unknown {
return JSON . parse ( stripJsonComments ( content ) ) ;
}
2026-02-03 14:53:31 -08:00
/**
* FOR TESTING PURPOSES ONLY.
* Clears the real path cache.
*/
export function clearRealPathCacheForTesting ( ) : void {
realPathCache . clear ( ) ;
}
function getRealPath ( location : string ) : string {
let realPath = realPathCache . get ( location ) ;
if ( realPath !== undefined ) {
return realPath ;
}
try {
realPath = fs . existsSync ( location ) ? fs . realpathSync ( location ) : location ;
} catch {
realPath = location ;
}
realPathCache . set ( location , realPath ) ;
return realPath ;
}
2025-08-13 11:06:31 -07:00
export class LoadedTrustedFolders {
constructor (
2025-08-28 15:16:07 -04:00
readonly user : TrustedFoldersFile ,
readonly errors : TrustedFoldersError [ ] ,
2025-08-13 11:06:31 -07:00
) { }
get rules ( ) : TrustRule [ ] {
return Object . entries ( this . user . config ) . map ( ( [ path , trustLevel ] ) = > ( {
path ,
trustLevel ,
} ) ) ;
}
2025-08-28 15:16:07 -04:00
/**
* Returns true or false if the path should be "trusted". This function
* should only be invoked when the folder trust setting is active.
*
* @param location path
* @returns
*/
2025-11-14 11:56:39 -08:00
isPathTrusted (
location : string ,
config? : Record < string , TrustLevel > ,
2026-02-11 16:20:54 -08:00
headlessOptions? : HeadlessModeOptions ,
2025-11-14 11:56:39 -08:00
) : boolean | undefined {
2026-02-11 16:20:54 -08:00
if ( isHeadlessMode ( headlessOptions ) ) {
return true ;
}
2025-11-14 11:56:39 -08:00
const configToUse = config ? ? this . user . config ;
2025-08-28 15:16:07 -04:00
2026-02-03 14:53:31 -08:00
// Resolve location to its realpath for canonical comparison
const realLocation = getRealPath ( location ) ;
2025-08-28 15:16:07 -04:00
2026-02-03 14:53:31 -08:00
let longestMatchLen = - 1 ;
let longestMatchTrust : TrustLevel | undefined = undefined ;
for ( const [ rulePath , trustLevel ] of Object . entries ( configToUse ) ) {
const effectivePath =
trustLevel === TrustLevel . TRUST_PARENT
? path . dirname ( rulePath )
: rulePath ;
// Resolve effectivePath to its realpath for canonical comparison
const realEffectivePath = getRealPath ( effectivePath ) ;
if ( isWithinRoot ( realLocation , realEffectivePath ) ) {
if ( rulePath . length > longestMatchLen ) {
longestMatchLen = rulePath . length ;
longestMatchTrust = trustLevel ;
}
2025-08-28 15:16:07 -04:00
}
}
2026-02-03 14:53:31 -08:00
if ( longestMatchTrust === TrustLevel . DO_NOT_TRUST ) return false ;
if (
longestMatchTrust === TrustLevel . TRUST_FOLDER ||
longestMatchTrust === TrustLevel . TRUST_PARENT
)
return true ;
2025-08-28 15:16:07 -04:00
return undefined ;
}
2026-02-09 09:16:56 -08:00
async setValue ( folderPath : string , trustLevel : TrustLevel ) : Promise < void > {
if ( this . errors . length > 0 ) {
const errorMessages = this . errors . map (
( error ) = > ` Error in ${ error . path } : ${ error . message } ` ,
) ;
throw new FatalConfigError (
` Cannot update trusted folders because the configuration file is invalid: \ n ${ errorMessages . join ( '\n' ) } \ nPlease fix the file manually before trying to update it. ` ,
) ;
}
const dirPath = path . dirname ( this . user . path ) ;
if ( ! fs . existsSync ( dirPath ) ) {
await fsPromises . mkdir ( dirPath , { recursive : true } ) ;
}
// lockfile requires the file to exist
if ( ! fs . existsSync ( this . user . path ) ) {
await fsPromises . writeFile ( this . user . path , JSON . stringify ( { } , null , 2 ) , {
mode : 0o600 ,
} ) ;
}
const release = await lock ( this . user . path , {
retries : {
retries : 10 ,
minTimeout : 100 ,
} ,
} ) ;
2025-11-14 11:56:39 -08:00
try {
2026-02-09 09:16:56 -08:00
// Re-read the file to handle concurrent updates
const content = await fsPromises . readFile ( this . user . path , 'utf-8' ) ;
let config : Record < string , TrustLevel > ;
try {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-02-09 09:16:56 -08:00
config = parseTrustedFoldersJson ( content ) as Record < string , TrustLevel > ;
} catch ( error ) {
coreEvents . emitFeedback (
'error' ,
` Failed to parse trusted folders file at ${ this . user . path } . The file may be corrupted. ` ,
error ,
) ;
config = { } ;
}
const originalTrustLevel = config [ folderPath ] ;
config [ folderPath ] = trustLevel ;
this . user . config [ folderPath ] = trustLevel ;
try {
saveTrustedFolders ( { . . . this . user , config } ) ;
} catch ( e ) {
// Revert the in-memory change if the save failed.
if ( originalTrustLevel === undefined ) {
delete this . user . config [ folderPath ] ;
} else {
this . user . config [ folderPath ] = originalTrustLevel ;
}
throw e ;
2025-11-14 11:56:39 -08:00
}
2026-02-09 09:16:56 -08:00
} finally {
await release ( ) ;
2025-11-14 11:56:39 -08:00
}
2025-08-13 11:06:31 -07:00
}
}
2025-09-22 17:06:43 -07:00
let loadedTrustedFolders : LoadedTrustedFolders | undefined ;
/**
* FOR TESTING PURPOSES ONLY.
* Resets the in-memory cache of the trusted folders configuration.
*/
export function resetTrustedFoldersForTesting ( ) : void {
loadedTrustedFolders = undefined ;
2026-02-03 14:53:31 -08:00
clearRealPathCacheForTesting ( ) ;
2025-09-22 17:06:43 -07:00
}
2025-08-13 11:06:31 -07:00
export function loadTrustedFolders ( ) : LoadedTrustedFolders {
2025-09-22 17:06:43 -07:00
if ( loadedTrustedFolders ) {
return loadedTrustedFolders ;
}
2025-08-13 11:06:31 -07:00
const errors : TrustedFoldersError [ ] = [ ] ;
2025-12-19 19:13:03 -05:00
const userConfig : Record < string , TrustLevel > = { } ;
2025-08-13 11:06:31 -07:00
2025-09-16 09:58:57 -07:00
const userPath = getTrustedFoldersPath ( ) ;
2025-08-13 11:06:31 -07:00
try {
if ( fs . existsSync ( userPath ) ) {
const content = fs . readFileSync ( userPath , 'utf-8' ) ;
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-02-09 09:16:56 -08:00
const parsed = parseTrustedFoldersJson ( content ) as Record < string , string > ;
2025-09-22 17:06:43 -07:00
if (
typeof parsed !== 'object' ||
parsed === null ||
Array . isArray ( parsed )
) {
errors . push ( {
message : 'Trusted folders file is not a valid JSON object.' ,
path : userPath ,
} ) ;
} else {
2025-12-19 19:13:03 -05:00
for ( const [ path , trustLevel ] of Object . entries ( parsed ) ) {
if ( isTrustLevel ( trustLevel ) ) {
userConfig [ path ] = trustLevel ;
} else {
const possibleValues = Object . values ( TrustLevel ) . join ( ', ' ) ;
errors . push ( {
message : ` Invalid trust level " ${ trustLevel } " for path " ${ path } ". Possible values are: ${ possibleValues } . ` ,
path : userPath ,
} ) ;
}
}
2025-08-13 11:06:31 -07:00
}
}
2026-02-03 14:53:31 -08:00
} catch ( error ) {
2025-08-13 11:06:31 -07:00
errors . push ( {
message : getErrorMessage ( error ) ,
path : userPath ,
} ) ;
}
2025-09-22 17:06:43 -07:00
loadedTrustedFolders = new LoadedTrustedFolders (
2025-08-13 11:06:31 -07:00
{ path : userPath , config : userConfig } ,
errors ,
) ;
2025-09-22 17:06:43 -07:00
return loadedTrustedFolders ;
2025-08-13 11:06:31 -07:00
}
export function saveTrustedFolders (
trustedFoldersFile : TrustedFoldersFile ,
) : void {
2025-11-14 11:56:39 -08:00
// Ensure the directory exists
const dirPath = path . dirname ( trustedFoldersFile . path ) ;
if ( ! fs . existsSync ( dirPath ) ) {
fs . mkdirSync ( dirPath , { recursive : true } ) ;
2025-08-13 11:06:31 -07:00
}
2025-11-14 11:56:39 -08:00
2026-02-09 09:16:56 -08:00
const content = JSON . stringify ( trustedFoldersFile . config , null , 2 ) ;
const tempPath = ` ${ trustedFoldersFile . path } .tmp. ${ crypto . randomUUID ( ) } ` ;
try {
fs . writeFileSync ( tempPath , content , {
encoding : 'utf-8' ,
mode : 0o600 ,
} ) ;
fs . renameSync ( tempPath , trustedFoldersFile . path ) ;
} catch ( error ) {
// Clean up temp file if it was created but rename failed
if ( fs . existsSync ( tempPath ) ) {
try {
fs . unlinkSync ( tempPath ) ;
} catch {
// Ignore cleanup errors
}
}
throw error ;
}
2025-08-13 11:06:31 -07:00
}
2025-08-28 15:16:07 -04:00
/** Is folder trust feature enabled per the current applied settings */
export function isFolderTrustEnabled ( settings : Settings ) : boolean {
2026-02-03 17:08:10 -08:00
const folderTrustSetting = settings . security ? . folderTrust ? . enabled ? ? true ;
2025-09-02 12:01:22 -04:00
return folderTrustSetting ;
2025-08-28 15:16:07 -04:00
}
2025-08-14 11:15:48 -07:00
2025-09-22 11:45:02 -07:00
function getWorkspaceTrustFromLocalConfig (
2026-02-03 00:54:10 -05:00
workspaceDir : string ,
2025-09-22 11:45:02 -07:00
trustConfig? : Record < string , TrustLevel > ,
2026-02-11 16:20:54 -08:00
headlessOptions? : HeadlessModeOptions ,
2025-09-22 11:45:02 -07:00
) : TrustResult {
2025-08-28 15:16:07 -04:00
const folders = loadTrustedFolders ( ) ;
2025-11-14 11:56:39 -08:00
const configToUse = trustConfig ? ? folders . user . config ;
2025-09-22 11:45:02 -07:00
2025-08-28 15:16:07 -04:00
if ( folders . errors . length > 0 ) {
2025-09-22 17:06:43 -07:00
const errorMessages = folders . errors . map (
( error ) = > ` Error in ${ error . path } : ${ error . message } ` ,
) ;
throw new FatalConfigError (
` ${ errorMessages . join ( '\n' ) } \ nPlease fix the configuration file and try again. ` ,
) ;
2025-08-13 11:06:31 -07:00
}
2026-02-11 16:20:54 -08:00
const isTrusted = folders . isPathTrusted (
workspaceDir ,
configToUse ,
headlessOptions ,
) ;
2025-09-22 11:45:02 -07:00
return {
isTrusted ,
source : isTrusted !== undefined ? 'file' : undefined ,
} ;
2025-08-13 11:06:31 -07:00
}
2025-09-03 11:44:26 -07:00
2025-09-22 11:45:02 -07:00
export function isWorkspaceTrusted (
settings : Settings ,
2026-02-03 00:54:10 -05:00
workspaceDir : string = process . cwd ( ) ,
2025-09-22 11:45:02 -07:00
trustConfig? : Record < string , TrustLevel > ,
2026-02-11 16:20:54 -08:00
headlessOptions? : HeadlessModeOptions ,
2025-09-22 11:45:02 -07:00
) : TrustResult {
2026-02-11 16:20:54 -08:00
if ( isHeadlessMode ( headlessOptions ) ) {
2026-02-09 15:46:49 -08:00
return { isTrusted : true , source : undefined } ;
}
2025-09-03 11:44:26 -07:00
if ( ! isFolderTrustEnabled ( settings ) ) {
2025-09-22 11:45:02 -07:00
return { isTrusted : true , source : undefined } ;
2025-09-03 11:44:26 -07:00
}
2025-09-11 13:07:57 -07:00
const ideTrust = ideContextStore . get ( ) ? . workspaceState ? . isTrusted ;
2025-09-03 11:44:26 -07:00
if ( ideTrust !== undefined ) {
2025-09-22 11:45:02 -07:00
return { isTrusted : ideTrust , source : 'ide' } ;
2025-09-03 11:44:26 -07:00
}
// Fall back to the local user configuration
2026-02-11 16:20:54 -08:00
return getWorkspaceTrustFromLocalConfig (
workspaceDir ,
trustConfig ,
headlessOptions ,
) ;
2025-09-03 11:44:26 -07:00
}