Download packages/core/src/services/FolderTrustDiscoveryService.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 6.94 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/services/FolderTrustDiscoveryService.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/services/FolderTrustDiscoveryService.ts
-
curl -L -o FolderTrustDiscoveryService.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/services/FolderTrustDiscoveryService.ts
6.94 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import * as fs from 'node:fs/promises'; | |
| import * as path from 'node:path'; | |
| import stripJsonComments from 'strip-json-comments'; | |
| import { GEMINI_DIR } from '../utils/paths.js'; | |
| import { debugLogger } from '../utils/debugLogger.js'; | |
| import { isNodeError } from '../utils/errors.js'; | |
| export interface FolderDiscoveryResults { | |
| commands: string[]; | |
| mcps: string[]; | |
| hooks: string[]; | |
| skills: string[]; | |
| agents: string[]; | |
| settings: string[]; | |
| securityWarnings: string[]; | |
| discoveryErrors: string[]; | |
| } | |
| /** | |
| * A safe, read-only service to discover local configurations in a folder | |
| * before it is trusted. | |
| */ | |
| export class FolderTrustDiscoveryService { | |
| /** | |
| * Discovers configurations in the given workspace directory. | |
| * @param workspaceDir The directory to scan. | |
| * @returns A summary of discovered configurations. | |
| */ | |
| static async discover(workspaceDir: string): Promise<FolderDiscoveryResults> { | |
| const results: FolderDiscoveryResults = { | |
| commands: [], | |
| mcps: [], | |
| hooks: [], | |
| skills: [], | |
| agents: [], | |
| settings: [], | |
| securityWarnings: [], | |
| discoveryErrors: [], | |
| }; | |
| const geminiDir = path.join(workspaceDir, GEMINI_DIR); | |
| if (!(await this.exists(geminiDir))) { | |
| return results; | |
| } | |
| await Promise.all([ | |
| this.discoverCommands(geminiDir, results), | |
| this.discoverSkills(geminiDir, results), | |
| this.discoverAgents(geminiDir, results), | |
| this.discoverSettings(geminiDir, results), | |
| ]); | |
| return results; | |
| } | |
| private static async discoverCommands( | |
| geminiDir: string, | |
| results: FolderDiscoveryResults, | |
| ) { | |
| const commandsDir = path.join(geminiDir, 'commands'); | |
| if (await this.exists(commandsDir)) { | |
| try { | |
| const files = await fs.readdir(commandsDir, { recursive: true }); | |
| results.commands = files | |
| .filter((f) => f.endsWith('.toml')) | |
| .map((f) => path.basename(f, '.toml')); | |
| } catch (e) { | |
| results.discoveryErrors.push( | |
| `Failed to discover commands: ${e instanceof Error ? e.message : String(e)}`, | |
| ); | |
| } | |
| } | |
| } | |
| private static async discoverSkills( | |
| geminiDir: string, | |
| results: FolderDiscoveryResults, | |
| ) { | |
| const skillsDir = path.join(geminiDir, 'skills'); | |
| if (await this.exists(skillsDir)) { | |
| try { | |
| const entries = await fs.readdir(skillsDir, { withFileTypes: true }); | |
| for (const entry of entries) { | |
| if (entry.isDirectory()) { | |
| const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md'); | |
| if (await this.exists(skillMdPath)) { | |
| results.skills.push(entry.name); | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| results.discoveryErrors.push( | |
| `Failed to discover skills: ${e instanceof Error ? e.message : String(e)}`, | |
| ); | |
| } | |
| } | |
| } | |
| private static async discoverAgents( | |
| geminiDir: string, | |
| results: FolderDiscoveryResults, | |
| ) { | |
| const agentsDir = path.join(geminiDir, 'agents'); | |
| if (await this.exists(agentsDir)) { | |
| try { | |
| const entries = await fs.readdir(agentsDir, { withFileTypes: true }); | |
| for (const entry of entries) { | |
| if ( | |
| entry.isFile() && | |
| entry.name.endsWith('.md') && | |
| !entry.name.startsWith('_') | |
| ) { | |
| results.agents.push(path.basename(entry.name, '.md')); | |
| } | |
| } | |
| if (results.agents.length > 0) { | |
| results.securityWarnings.push('This project contains custom agents.'); | |
| } | |
| } catch (e) { | |
| results.discoveryErrors.push( | |
| `Failed to discover agents: ${e instanceof Error ? e.message : String(e)}`, | |
| ); | |
| } | |
| } | |
| } | |
| private static async discoverSettings( | |
| geminiDir: string, | |
| results: FolderDiscoveryResults, | |
| ) { | |
| const settingsPath = path.join(geminiDir, 'settings.json'); | |
| if (!(await this.exists(settingsPath))) return; | |
| try { | |
| const content = await fs.readFile(settingsPath, 'utf-8'); | |
| const settings = JSON.parse(stripJsonComments(content)) as unknown; | |
| if (!this.isRecord(settings)) { | |
| debugLogger.debug('Settings must be a JSON object'); | |
| return; | |
| } | |
| results.settings = Object.keys(settings).filter( | |
| (key) => !['mcpServers', 'hooks', '$schema'].includes(key), | |
| ); | |
| results.securityWarnings.push(...this.collectSecurityWarnings(settings)); | |
| const mcpServers = settings['mcpServers']; | |
| if (this.isRecord(mcpServers)) { | |
| results.mcps = Object.keys(mcpServers); | |
| } | |
| const hooksConfig = settings['hooks']; | |
| if (this.isRecord(hooksConfig)) { | |
| const hooks = new Set<string>(); | |
| for (const event of Object.values(hooksConfig)) { | |
| if (!Array.isArray(event)) continue; | |
| for (const hook of event) { | |
| // eslint-disable-next-line no-restricted-syntax | |
| if (this.isRecord(hook) && typeof hook['command'] === 'string') { | |
| hooks.add(hook['command']); | |
| } | |
| } | |
| } | |
| results.hooks = Array.from(hooks); | |
| } | |
| } catch (e) { | |
| results.discoveryErrors.push( | |
| `Failed to discover settings: ${e instanceof Error ? e.message : String(e)}`, | |
| ); | |
| } | |
| } | |
| private static collectSecurityWarnings( | |
| settings: Record<string, unknown>, | |
| ): string[] { | |
| const warnings: string[] = []; | |
| const tools = this.isRecord(settings['tools']) | |
| ? settings['tools'] | |
| : undefined; | |
| const security = this.isRecord(settings['security']) | |
| ? settings['security'] | |
| : undefined; | |
| const folderTrust = | |
| security && this.isRecord(security['folderTrust']) | |
| ? security['folderTrust'] | |
| : undefined; | |
| const allowedTools = tools?.['allowed']; | |
| const checks = [ | |
| { | |
| condition: Array.isArray(allowedTools) && allowedTools.length > 0, | |
| message: 'This project auto-approves certain tools (tools.allowed).', | |
| }, | |
| { | |
| condition: folderTrust?.['enabled'] === false, | |
| message: | |
| 'This project attempts to disable folder trust (security.folderTrust.enabled).', | |
| }, | |
| { | |
| condition: tools?.['sandbox'] === false, | |
| message: 'This project disables the security sandbox (tools.sandbox).', | |
| }, | |
| ]; | |
| for (const check of checks) { | |
| if (check.condition) warnings.push(check.message); | |
| } | |
| return warnings; | |
| } | |
| private static isRecord(val: unknown): val is Record<string, unknown> { | |
| return !!val && typeof val === 'object' && !Array.isArray(val); | |
| } | |
| private static async exists(filePath: string): Promise<boolean> { | |
| try { | |
| await fs.stat(filePath); | |
| return true; | |
| } catch (e) { | |
| if (isNodeError(e) && e.code === 'ENOENT') { | |
| return false; | |
| } | |
| throw e; | |
| } | |
| } | |
| } | |