Download packages/core/src/config/config.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 134 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/config/config.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/config/config.ts
-
curl -L -o config.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/config/config.ts
134 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import * as fs from 'node:fs'; | |
| import * as path from 'node:path'; | |
| import { SandboxPolicyManager } from '../policy/sandboxPolicyManager.js'; | |
| import { inspect } from 'node:util'; | |
| import process from 'node:process'; | |
| import { z } from 'zod'; | |
| import type { ConversationRecord } from '../services/chatRecordingService.js'; | |
| import type { | |
| AgentHistoryProviderConfig, | |
| ContextManagementConfig, | |
| ToolOutputMaskingConfig, | |
| } from '../context/types.js'; | |
| export type { ConversationRecord }; | |
| import { | |
| AuthType, | |
| createContentGenerator, | |
| createContentGeneratorConfig, | |
| type ContentGenerator, | |
| type ContentGeneratorConfig, | |
| type VertexAiRoutingConfig, | |
| } from '../core/contentGenerator.js'; | |
| import type { OverageStrategy } from '../billing/billing.js'; | |
| import { PromptRegistry } from '../prompts/prompt-registry.js'; | |
| import { ResourceRegistry } from '../resources/resource-registry.js'; | |
| import { ToolRegistry } from '../tools/tool-registry.js'; | |
| import { LSTool } from '../tools/ls.js'; | |
| import { ReadFileTool } from '../tools/read-file.js'; | |
| import { ReadMcpResourceTool } from '../tools/read-mcp-resource.js'; | |
| import { ListMcpResourcesTool } from '../tools/list-mcp-resources.js'; | |
| import { GrepTool } from '../tools/grep.js'; | |
| import { RipGrepTool, resolveRipgrepPath } from '../tools/ripGrep.js'; | |
| import { GlobTool } from '../tools/glob.js'; | |
| import { ActivateSkillTool } from '../tools/activate-skill.js'; | |
| import { EditTool } from '../tools/edit.js'; | |
| import { ShellTool } from '../tools/shell.js'; | |
| import { WriteFileTool } from '../tools/write-file.js'; | |
| import { WebFetchTool } from '../tools/web-fetch.js'; | |
| import { | |
| setGeminiMdFilename, | |
| getCurrentGeminiMdFilename, | |
| } from '../tools/memoryTool.js'; | |
| import { WebSearchTool } from '../tools/web-search.js'; | |
| import { AskUserTool } from '../tools/ask-user.js'; | |
| import { UpdateTopicTool } from '../tools/topicTool.js'; | |
| import { TopicState } from './topicState.js'; | |
| import { AgentTool } from '../agents/agent-tool.js'; | |
| import { ExitPlanModeTool } from '../tools/exit-plan-mode.js'; | |
| import { EnterPlanModeTool } from '../tools/enter-plan-mode.js'; | |
| import { | |
| ListBackgroundProcessesTool, | |
| ReadBackgroundOutputTool, | |
| } from '../tools/shellBackgroundTools.js'; | |
| import { GeminiClient } from '../core/client.js'; | |
| import { BaseLlmClient } from '../core/baseLlmClient.js'; | |
| import { LocalLiteRtLmClient } from '../core/localLiteRtLmClient.js'; | |
| import type { HookDefinition, HookEventName } from '../hooks/types.js'; | |
| import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; | |
| import { GitService } from '../services/gitService.js'; | |
| import { | |
| type SandboxManager, | |
| NoopSandboxManager, | |
| } from '../services/sandboxManager.js'; | |
| import { createSandboxManager } from '../services/sandboxManagerFactory.js'; | |
| import { SandboxedFileSystemService } from '../services/sandboxedFileSystemService.js'; | |
| import { | |
| initializeTelemetry, | |
| DEFAULT_TELEMETRY_TARGET, | |
| DEFAULT_OTLP_ENDPOINT, | |
| uiTelemetryService, | |
| type TelemetryTarget, | |
| } from '../telemetry/index.js'; | |
| import { coreEvents, CoreEvent } from '../utils/events.js'; | |
| import { tokenLimit } from '../core/tokenLimits.js'; | |
| import { | |
| DEFAULT_GEMINI_EMBEDDING_MODEL, | |
| DEFAULT_GEMINI_FLASH_MODEL, | |
| DEFAULT_GEMINI_MODEL_AUTO, | |
| isAutoModel, | |
| isPreviewModel, | |
| isGemini2Model, | |
| PREVIEW_GEMINI_FLASH_MODEL, | |
| resolveModel, | |
| setFlashModels, | |
| DEFAULT_GEMINI_3_5_FLASH_MODEL, | |
| SECONDARY_GEMINI_3_5_FLASH_MODEL, | |
| } from './models.js'; | |
| import { shouldAttemptBrowserLaunch } from '../utils/browser.js'; | |
| import type { MCPOAuthConfig } from '../mcp/oauth-provider.js'; | |
| import { checkPathTrust } from '../utils/trust.js'; | |
| import { WriteTodosTool } from '../tools/write-todos.js'; | |
| import { | |
| StandardFileSystemService, | |
| type FileSystemService, | |
| } from '../services/fileSystemService.js'; | |
| import { | |
| TrackerCreateTaskTool, | |
| TrackerUpdateTaskTool, | |
| TrackerGetTaskTool, | |
| TrackerListTasksTool, | |
| TrackerAddDependencyTool, | |
| TrackerVisualizeTool, | |
| } from '../tools/trackerTools.js'; | |
| import { | |
| logRipgrepFallback, | |
| logFlashFallback, | |
| logApprovalModeSwitch, | |
| logApprovalModeDuration, | |
| } from '../telemetry/loggers.js'; | |
| import { | |
| RipgrepFallbackEvent, | |
| FlashFallbackEvent, | |
| ApprovalModeSwitchEvent, | |
| ApprovalModeDurationEvent, | |
| } from '../telemetry/types.js'; | |
| import type { | |
| FallbackModelHandler, | |
| ValidationHandler, | |
| } from '../fallback/types.js'; | |
| import { ModelAvailabilityService } from '../availability/modelAvailabilityService.js'; | |
| import { ModelRouterService } from '../routing/modelRouterService.js'; | |
| import { OutputFormat } from '../output/types.js'; | |
| import { | |
| ModelConfigService, | |
| type ModelConfig, | |
| type ModelConfigServiceConfig, | |
| } from '../services/modelConfigService.js'; | |
| import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js'; | |
| import { MemoryContextManager } from '../context/memoryContextManager.js'; | |
| import { TrackerService } from '../services/trackerService.js'; | |
| import type { GenerateContentParameters } from '@google/genai'; | |
| // Re-export OAuth config type | |
| export type { MCPOAuthConfig, AnyToolInvocation, AnyDeclarativeTool }; | |
| import type { AnyToolInvocation, AnyDeclarativeTool } from '../tools/tools.js'; | |
| import { WorkspaceContext } from '../utils/workspaceContext.js'; | |
| import { | |
| getWorkspaceContextOverride, | |
| hasScopedAutoMemoryExtractionWriteAccess, | |
| hasScopedMemoryInboxAccess, | |
| } from './scoped-config.js'; | |
| import { Storage } from './storage.js'; | |
| import type { ShellExecutionConfig } from '../services/shellExecutionService.js'; | |
| import { FileExclusions } from '../utils/ignorePatterns.js'; | |
| import { MessageBus } from '../confirmation-bus/message-bus.js'; | |
| import type { EventEmitter } from 'node:events'; | |
| import { PolicyEngine } from '../policy/policy-engine.js'; | |
| import { | |
| ApprovalMode, | |
| type PolicyEngineConfig, | |
| type PolicyRule, | |
| type SafetyCheckerRule, | |
| } from '../policy/types.js'; | |
| import { HookSystem } from '../hooks/index.js'; | |
| import type { | |
| UserTierId, | |
| GeminiUserTier, | |
| RetrieveUserQuotaResponse, | |
| AdminControlsSettings, | |
| } from '../code_assist/types.js'; | |
| import type { HierarchicalMemory } from './memory.js'; | |
| import { getCodeAssistServer } from '../code_assist/codeAssist.js'; | |
| import { | |
| getExperiments, | |
| type Experiments, | |
| } from '../code_assist/experiments/experiments.js'; | |
| import { AgentRegistry } from '../agents/registry.js'; | |
| import { AcknowledgedAgentsService } from '../agents/acknowledgedAgents.js'; | |
| import { setGlobalProxy, updateGlobalFetchTimeouts } from '../utils/fetch.js'; | |
| import { ExperimentFlags } from '../code_assist/experiments/flagNames.js'; | |
| import { debugLogger } from '../utils/debugLogger.js'; | |
| import { ragLogger } from '../utils/ragLogger.js'; | |
| import { SkillManager, type SkillDefinition } from '../skills/skillManager.js'; | |
| import { startupProfiler } from '../telemetry/startupProfiler.js'; | |
| import type { AgentDefinition } from '../agents/types.js'; | |
| import { fetchAdminControls } from '../code_assist/admin/admin_controls.js'; | |
| import { isSubpath, resolveToRealPath } from '../utils/paths.js'; | |
| import { validatePath } from '../utils/path-validator.js'; | |
| import { InjectionService } from './injectionService.js'; | |
| import { ExecutionLifecycleService } from '../services/executionLifecycleService.js'; | |
| import { WORKSPACE_POLICY_TIER } from '../policy/config.js'; | |
| import { loadPoliciesFromToml } from '../policy/toml-loader.js'; | |
| import { CheckerRunner } from '../safety/checker-runner.js'; | |
| import { ContextBuilder } from '../safety/context-builder.js'; | |
| import { CheckerRegistry } from '../safety/registry.js'; | |
| import { ConsecaSafetyChecker } from '../safety/conseca/conseca.js'; | |
| import type { AgentLoopContext } from './agent-loop-context.js'; | |
| export interface AccessibilitySettings { | |
| /** @deprecated Use ui.statusHints instead. */ | |
| enableLoadingPhrases?: boolean; | |
| screenReader?: boolean; | |
| } | |
| export interface BugCommandSettings { | |
| urlTemplate: string; | |
| } | |
| export interface SummarizeToolOutputSettings { | |
| tokenBudget?: number; | |
| } | |
| export interface PlanSettings { | |
| enabled?: boolean; | |
| directory?: string; | |
| modelRouting?: boolean; | |
| } | |
| export interface TelemetrySettings { | |
| enabled?: boolean; | |
| traces?: boolean; | |
| target?: TelemetryTarget; | |
| otlpEndpoint?: string; | |
| otlpProtocol?: 'grpc' | 'http'; | |
| logPrompts?: boolean; | |
| outfile?: string; | |
| useCollector?: boolean; | |
| useCliAuth?: boolean; | |
| } | |
| export interface OutputSettings { | |
| format?: OutputFormat; | |
| } | |
| export interface GemmaModelRouterSettings { | |
| enabled?: boolean; | |
| autoStartServer?: boolean; | |
| binaryPath?: string; | |
| classifier?: { | |
| host?: string; | |
| model?: string; | |
| }; | |
| } | |
| export interface ADKSettings { | |
| agentSessionNoninteractiveEnabled?: boolean; | |
| agentSessionInteractiveEnabled?: boolean; | |
| agentSessionSubagentEnabled?: boolean; | |
| } | |
| export interface ExtensionSetting { | |
| name: string; | |
| description: string; | |
| envVar: string; | |
| sensitive?: boolean; | |
| } | |
| export interface ResolvedExtensionSetting { | |
| name: string; | |
| envVar: string; | |
| value?: string; | |
| sensitive: boolean; | |
| scope?: 'user' | 'workspace'; | |
| source?: string; | |
| } | |
| export interface TrajectoryProvider { | |
| /** Prefix used to identify sessions from this provider (e.g., 'ext:') */ | |
| prefix: string; | |
| /** Optional display name for UI Tabs */ | |
| displayName?: string; | |
| /** Return an array of conversational tags/ids */ | |
| listSessions(workspaceUri?: string): Promise< | |
| Array<{ | |
| id: string; | |
| mtime: string; | |
| name?: string; | |
| displayName?: string; | |
| messageCount?: number; | |
| }> | |
| >; | |
| /** Load a single conversation payload */ | |
| loadSession(id: string): Promise<ConversationRecord | null>; | |
| } | |
| export interface AgentRunConfig { | |
| maxTimeMinutes?: number; | |
| maxTurns?: number; | |
| } | |
| /** | |
| * Override configuration for a specific agent. | |
| * Generic fields (modelConfig, runConfig, enabled) are standard across all agents. | |
| */ | |
| export interface AgentOverride { | |
| modelConfig?: ModelConfig; | |
| runConfig?: AgentRunConfig; | |
| enabled?: boolean; | |
| tools?: string[]; | |
| mcpServers?: Record<string, MCPServerConfig>; | |
| } | |
| export interface AgentSettings { | |
| overrides?: Record<string, AgentOverride>; | |
| browser?: BrowserAgentCustomConfig; | |
| } | |
| export interface CustomTheme { | |
| type: 'custom'; | |
| name: string; | |
| text?: { | |
| primary?: string; | |
| secondary?: string; | |
| link?: string; | |
| accent?: string; | |
| response?: string; | |
| }; | |
| background?: { | |
| primary?: string; | |
| diff?: { | |
| added?: string; | |
| removed?: string; | |
| }; | |
| }; | |
| border?: { | |
| default?: string; | |
| }; | |
| ui?: { | |
| comment?: string; | |
| symbol?: string; | |
| active?: string; | |
| focus?: string; | |
| gradient?: string[]; | |
| }; | |
| status?: { | |
| error?: string; | |
| success?: string; | |
| warning?: string; | |
| }; | |
| // Legacy properties (all optional) | |
| Background?: string; | |
| Foreground?: string; | |
| LightBlue?: string; | |
| AccentBlue?: string; | |
| AccentPurple?: string; | |
| AccentCyan?: string; | |
| AccentGreen?: string; | |
| AccentYellow?: string; | |
| AccentRed?: string; | |
| DiffAdded?: string; | |
| DiffRemoved?: string; | |
| Comment?: string; | |
| Gray?: string; | |
| DarkGray?: string; | |
| GradientColors?: string[]; | |
| } | |
| /** | |
| * Browser agent custom configuration. | |
| * Used in agents.browser | |
| * | |
| * IMPORTANT: Keep in sync with the browser settings schema in | |
| * packages/cli/src/config/settingsSchema.ts (agents.browser.properties). | |
| */ | |
| export interface BrowserAgentCustomConfig { | |
| /** | |
| * Session mode: | |
| * - 'persistent': Launch Chrome with a persistent profile at ~/.cache/chrome-devtools-mcp/ (default) | |
| * - 'isolated': Launch Chrome with a temporary profile, cleaned up after session | |
| * - 'existing': Attach to an already-running Chrome instance (requires remote debugging | |
| * enabled at chrome://inspect/#remote-debugging) | |
| */ | |
| sessionMode?: 'isolated' | 'persistent' | 'existing'; | |
| /** Run browser in headless mode. Default: false */ | |
| headless?: boolean; | |
| /** Path to Chrome profile directory for session persistence. */ | |
| profilePath?: string; | |
| /** Model for the visual agent's analyze_screenshot tool. When set, enables the tool. */ | |
| visualModel?: string; | |
| /** List of allowed domains for the browser agent (e.g., ["github.com", "*.google.com"]). */ | |
| allowedDomains?: string[]; | |
| /** Disable user input on the browser window during automation. Default: true in non-headless mode */ | |
| disableUserInput?: boolean; | |
| /** Maximum number of actions (tool calls) allowed per task. Default: 100 */ | |
| maxActionsPerTask?: number; | |
| /** Whether to confirm sensitive actions (e.g., fill_form, evaluate_script). */ | |
| confirmSensitiveActions?: boolean; | |
| /** Whether to block file uploads. */ | |
| blockFileUploads?: boolean; | |
| } | |
| /** | |
| * All information required in CLI to handle an extension. Defined in Core so | |
| * that the collection of loaded, active, and inactive extensions can be passed | |
| * around on the config object though Core does not use this information | |
| * directly. | |
| */ | |
| export interface GeminiCLIExtension { | |
| name: string; | |
| version: string; | |
| isActive: boolean; | |
| path: string; | |
| installMetadata?: ExtensionInstallMetadata; | |
| mcpServers?: Record<string, MCPServerConfig>; | |
| contextFiles: string[]; | |
| excludeTools?: string[]; | |
| id: string; | |
| hooks?: { [K in HookEventName]?: HookDefinition[] }; | |
| settings?: ExtensionSetting[]; | |
| resolvedSettings?: ResolvedExtensionSetting[]; | |
| skills?: SkillDefinition[]; | |
| agents?: AgentDefinition[]; | |
| /** | |
| * Custom themes contributed by this extension. | |
| * These themes will be registered when the extension is activated. | |
| */ | |
| themes?: CustomTheme[]; | |
| /** | |
| * Policy rules contributed by this extension. | |
| */ | |
| rules?: PolicyRule[]; | |
| /** | |
| * Safety checkers contributed by this extension. | |
| */ | |
| checkers?: SafetyCheckerRule[]; | |
| /** | |
| * Planning features configuration contributed by this extension. | |
| */ | |
| plan?: { | |
| /** | |
| * The directory where planning artifacts are stored. | |
| */ | |
| directory?: string; | |
| }; | |
| /** | |
| * Used to migrate an extension to a new repository source. | |
| */ | |
| migratedTo?: string; | |
| /** Loaded JS module for trajectory decoding */ | |
| trajectoryProviderModule?: TrajectoryProvider; | |
| } | |
| export interface ExtensionInstallMetadata { | |
| source: string; | |
| type: 'git' | 'local' | 'link' | 'github-release'; | |
| releaseTag?: string; // Only present for github-release installs. | |
| ref?: string; | |
| autoUpdate?: boolean; | |
| allowPreRelease?: boolean; | |
| } | |
| import { getChannelFromVersion } from '../utils/channel.js'; | |
| import { DEFAULT_MAX_ATTEMPTS } from '../utils/retry.js'; | |
| import { | |
| DEFAULT_FILE_FILTERING_OPTIONS, | |
| DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, | |
| type FileFilteringOptions, | |
| } from './constants.js'; | |
| import { | |
| DEFAULT_TOOL_PROTECTION_THRESHOLD, | |
| DEFAULT_MIN_PRUNABLE_TOKENS_THRESHOLD, | |
| DEFAULT_PROTECT_LATEST_TURN, | |
| } from '../context/toolOutputMaskingService.js'; | |
| import { | |
| type ExtensionLoader, | |
| SimpleExtensionLoader, | |
| } from '../utils/extensionLoader.js'; | |
| import { McpClientManager } from '../tools/mcp-client-manager.js'; | |
| import { A2AClientManager } from '../agents/a2a-client-manager.js'; | |
| import { type McpContext } from '../tools/mcp-client.js'; | |
| import type { EnvironmentSanitizationConfig } from '../services/environmentSanitization.js'; | |
| export type { FileFilteringOptions }; | |
| export { | |
| DEFAULT_FILE_FILTERING_OPTIONS, | |
| DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, | |
| }; | |
| export const DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 40_000; | |
| export class MCPServerConfig { | |
| constructor( | |
| // For stdio transport | |
| readonly command?: string, | |
| readonly args?: string[], | |
| readonly env?: Record<string, string>, | |
| readonly cwd?: string, | |
| // For sse transport | |
| readonly url?: string, | |
| // For streamable http transport | |
| readonly httpUrl?: string, | |
| readonly headers?: Record<string, string>, | |
| // For websocket transport | |
| readonly tcp?: string, | |
| // Transport type (optional, for use with 'url' field) | |
| // When set to 'http', uses StreamableHTTPClientTransport | |
| // When set to 'sse', uses SSEClientTransport | |
| // When omitted, auto-detects transport type | |
| // Note: 'httpUrl' is deprecated in favor of 'url' + 'type' | |
| readonly type?: 'sse' | 'http', | |
| // Common | |
| readonly timeout?: number, | |
| readonly trust?: boolean, | |
| // Metadata | |
| readonly description?: string, | |
| readonly includeTools?: string[], | |
| readonly excludeTools?: string[], | |
| readonly extension?: GeminiCLIExtension, | |
| // OAuth configuration | |
| readonly oauth?: MCPOAuthConfig, | |
| readonly authProviderType?: AuthProviderType, | |
| // Service Account Configuration | |
| /* targetAudience format: CLIENT_ID.apps.googleusercontent.com */ | |
| readonly targetAudience?: string, | |
| /* targetServiceAccount format: <service-account-name>@<project-num>.iam.gserviceaccount.com */ | |
| readonly targetServiceAccount?: string, | |
| ) {} | |
| } | |
| export enum AuthProviderType { | |
| DYNAMIC_DISCOVERY = 'dynamic_discovery', | |
| GOOGLE_CREDENTIALS = 'google_credentials', | |
| SERVICE_ACCOUNT_IMPERSONATION = 'service_account_impersonation', | |
| } | |
| export interface SandboxConfig { | |
| enabled: boolean; | |
| allowedPaths?: string[]; | |
| includeDirectories?: string[]; | |
| networkAccess?: boolean; | |
| command?: | |
| | 'docker' | |
| | 'podman' | |
| | 'sandbox-exec' | |
| | 'runsc' | |
| | 'lxc' | |
| | 'windows-native'; | |
| image?: string; | |
| } | |
| export const ConfigSchema = z.object({ | |
| sandbox: z | |
| .object({ | |
| enabled: z.boolean().default(false), | |
| allowedPaths: z.array(z.string()).default([]), | |
| includeDirectories: z.array(z.string()).default([]), | |
| networkAccess: z.boolean().default(false), | |
| command: z | |
| .enum([ | |
| 'docker', | |
| 'podman', | |
| 'sandbox-exec', | |
| 'runsc', | |
| 'lxc', | |
| 'windows-native', | |
| ]) | |
| .optional(), | |
| image: z.string().optional(), | |
| }) | |
| .superRefine((data, ctx) => { | |
| if (data.enabled && !data.command) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'Sandbox command is required when sandbox is enabled', | |
| path: ['command'], | |
| }); | |
| } | |
| }) | |
| .optional(), | |
| }); | |
| /** | |
| * Callbacks for checking MCP server enablement status. | |
| * These callbacks are provided by the CLI package to bridge | |
| * the enablement state to the core package. | |
| */ | |
| export interface McpEnablementCallbacks { | |
| /** Check if a server is disabled for the current session only */ | |
| isSessionDisabled: (serverId: string) => boolean; | |
| /** Check if a server is enabled in the file-based configuration */ | |
| isFileEnabled: (serverId: string) => Promise<boolean>; | |
| } | |
| export interface PolicyUpdateConfirmationRequest { | |
| scope: string; | |
| identifier: string; | |
| policyDir: string; | |
| newHash: string; | |
| } | |
| export interface WorktreeSettings { | |
| name: string; | |
| path: string; | |
| baseSha: string; | |
| } | |
| export interface ConfigParameters { | |
| sessionId: string; | |
| clientName?: string; | |
| clientVersion?: string; | |
| embeddingModel?: string; | |
| sandbox?: SandboxConfig; | |
| toolSandboxing?: boolean; | |
| targetDir: string; | |
| debugMode: boolean; | |
| question?: string; | |
| coreTools?: string[]; | |
| mainAgentTools?: string[]; | |
| /** @deprecated Use Policy Engine instead */ | |
| allowedTools?: string[]; | |
| /** @deprecated Use Policy Engine instead */ | |
| excludeTools?: string[]; | |
| toolDiscoveryCommand?: string; | |
| toolCallCommand?: string; | |
| mcpServerCommand?: string; | |
| mcpServers?: Record<string, MCPServerConfig>; | |
| mcpEnablementCallbacks?: McpEnablementCallbacks; | |
| userMemory?: string | HierarchicalMemory; | |
| geminiMdFileCount?: number; | |
| geminiMdFilePaths?: string[]; | |
| approvalMode?: ApprovalMode; | |
| showMemoryUsage?: boolean; | |
| contextFileName?: string | string[]; | |
| accessibility?: AccessibilitySettings; | |
| telemetry?: TelemetrySettings; | |
| usageStatisticsEnabled?: boolean; | |
| fileFiltering?: { | |
| respectGitIgnore?: boolean; | |
| respectGeminiIgnore?: boolean; | |
| enableFileWatcher?: boolean; | |
| enableRecursiveFileSearch?: boolean; | |
| enableFuzzySearch?: boolean; | |
| maxFileCount?: number; | |
| searchTimeout?: number; | |
| customIgnoreFilePaths?: string[]; | |
| }; | |
| checkpointing?: boolean; | |
| proxy?: string; | |
| cwd: string; | |
| fileDiscoveryService?: FileDiscoveryService; | |
| includeDirectories?: string[]; | |
| bugCommand?: BugCommandSettings; | |
| model: string; | |
| disableLoopDetection?: boolean; | |
| maxSessionTurns?: number; | |
| acpMode?: boolean; | |
| listSessions?: boolean; | |
| deleteSession?: string; | |
| listExtensions?: boolean; | |
| extensionLoader?: ExtensionLoader; | |
| enabledExtensions?: string[]; | |
| enableExtensionReloading?: boolean; | |
| allowedMcpServers?: string[]; | |
| blockedMcpServers?: string[]; | |
| allowedEnvironmentVariables?: string[]; | |
| blockedEnvironmentVariables?: string[]; | |
| enableEnvironmentVariableRedaction?: boolean; | |
| noBrowser?: boolean; | |
| summarizeToolOutput?: Record<string, SummarizeToolOutputSettings>; | |
| folderTrust?: boolean; | |
| ideMode?: boolean; | |
| loadMemoryFromIncludeDirectories?: boolean; | |
| includeDirectoryTree?: boolean; | |
| importFormat?: 'tree' | 'flat'; | |
| discoveryMaxDirs?: number; | |
| compressionThreshold?: number; | |
| interactive?: boolean; | |
| trustedFolder?: boolean; | |
| useBackgroundColor?: boolean; | |
| useAlternateBuffer?: boolean; | |
| useTerminalBuffer?: boolean; | |
| useRenderProcess?: boolean; | |
| useRipgrep?: boolean; | |
| enableInteractiveShell?: boolean; | |
| shellBackgroundCompletionBehavior?: string; | |
| skipNextSpeakerCheck?: boolean; | |
| shellExecutionConfig?: ShellExecutionConfig; | |
| extensionManagement?: boolean; | |
| extensionRegistryURI?: string; | |
| truncateToolOutputThreshold?: number; | |
| eventEmitter?: EventEmitter; | |
| useWriteTodos?: boolean; | |
| env?: Record<string, string>; | |
| workspacePoliciesDir?: string; | |
| policyEngineConfig?: PolicyEngineConfig; | |
| directWebFetch?: boolean; | |
| policyUpdateConfirmationRequest?: PolicyUpdateConfirmationRequest; | |
| output?: OutputSettings; | |
| gemmaModelRouter?: GemmaModelRouterSettings; | |
| adk?: ADKSettings; | |
| disableModelRouterForAuth?: AuthType[]; | |
| retryFetchErrors?: boolean; | |
| maxAttempts?: number; | |
| enableShellOutputEfficiency?: boolean; | |
| shellToolInactivityTimeout?: number; | |
| fakeResponses?: string; | |
| fakeResponsesNonStrict?: string; | |
| recordResponses?: string; | |
| ptyInfo?: string; | |
| disableYoloMode?: boolean; | |
| disableAlwaysAllow?: boolean; | |
| voiceMode?: boolean; | |
| rawOutput?: boolean; | |
| acceptRawOutputRisk?: boolean; | |
| dynamicModelConfiguration?: boolean; | |
| modelConfigServiceConfig?: ModelConfigServiceConfig; | |
| enableHooks?: boolean; | |
| enableHooksUI?: boolean; | |
| experiments?: Experiments; | |
| contextManagement?: Partial<ContextManagementConfig>; | |
| hooks?: { [K in HookEventName]?: HookDefinition[] }; | |
| disabledHooks?: string[]; | |
| projectHooks?: { [K in HookEventName]?: HookDefinition[] }; | |
| enableAgents?: boolean; | |
| enableEventDrivenScheduler?: boolean; | |
| skillsSupport?: boolean; | |
| disabledSkills?: string[]; | |
| adminSkillsEnabled?: boolean; | |
| autoDistillation?: boolean; | |
| experimentalAutoMemory?: boolean; | |
| experimentalGemma?: boolean; | |
| experimentalContextManagementConfig?: string; | |
| experimentalAgentHistoryTruncation?: boolean; | |
| experimentalAgentHistoryTruncationThreshold?: number; | |
| experimentalAgentHistoryRetainedMessages?: number; | |
| experimentalAgentHistorySummarization?: boolean; | |
| memoryBoundaryMarkers?: string[]; | |
| topicUpdateNarration?: boolean; | |
| disableLLMCorrection?: boolean; | |
| plan?: boolean; | |
| tracker?: boolean; | |
| planSettings?: PlanSettings; | |
| worktreeSettings?: WorktreeSettings; | |
| modelSteering?: boolean; | |
| onModelChange?: (model: string) => void; | |
| mcpEnabled?: boolean; | |
| extensionsEnabled?: boolean; | |
| agents?: AgentSettings; | |
| onReload?: () => Promise<{ | |
| disabledSkills?: string[]; | |
| adminSkillsEnabled?: boolean; | |
| agents?: AgentSettings; | |
| }>; | |
| enableConseca?: boolean; | |
| billing?: { | |
| overageStrategy?: OverageStrategy; | |
| }; | |
| vertexAiRouting?: VertexAiRoutingConfig; | |
| logRagSnippets?: boolean; | |
| } | |
| export class Config implements McpContext, AgentLoopContext { | |
| readonly config: Config = this; | |
| promptId: string; | |
| toolRegistry!: ToolRegistry; | |
| promptRegistry!: PromptRegistry; | |
| resourceRegistry!: ResourceRegistry; | |
| private mcpClientManager?: McpClientManager; | |
| private readonly a2aClientManager?: A2AClientManager; | |
| private allowedMcpServers: string[]; | |
| private blockedMcpServers: string[]; | |
| private allowedEnvironmentVariables: string[]; | |
| private blockedEnvironmentVariables: string[]; | |
| private readonly enableEnvironmentVariableRedaction: boolean; | |
| private agentRegistry!: AgentRegistry; | |
| private readonly acknowledgedAgentsService: AcknowledgedAgentsService; | |
| private skillManager!: SkillManager; | |
| private _sessionId: string; | |
| private readonly clientName: string | undefined; | |
| private _clientVersion: string; | |
| private fileSystemService: FileSystemService; | |
| private trackerService?: TrackerService; | |
| readonly topicState = new TopicState(); | |
| private contentGeneratorConfig!: ContentGeneratorConfig; | |
| private contentGenerator!: ContentGenerator; | |
| readonly modelConfigService: ModelConfigService; | |
| private readonly embeddingModel: string; | |
| private readonly sandbox: SandboxConfig | undefined; | |
| private _sandboxForbiddenPaths: string[] | undefined; | |
| private readonly targetDir: string; | |
| private _ripgrepPathPromise?: Promise<string | null>; | |
| private workspaceContext: WorkspaceContext; | |
| private readonly debugMode: boolean; | |
| private readonly question: string | undefined; | |
| private readonly worktreeSettings: WorktreeSettings | undefined; | |
| readonly enableConseca: boolean; | |
| private readonly coreTools: string[] | undefined; | |
| private readonly mainAgentTools: string[] | undefined; | |
| /** @deprecated Use Policy Engine instead */ | |
| private readonly allowedTools: string[] | undefined; | |
| /** @deprecated Use Policy Engine instead */ | |
| private readonly excludeTools: string[] | undefined; | |
| private readonly toolDiscoveryCommand: string | undefined; | |
| private readonly toolCallCommand: string | undefined; | |
| private readonly mcpServerCommand: string | undefined; | |
| private readonly mcpEnabled: boolean; | |
| private readonly extensionsEnabled: boolean; | |
| private mcpServers: Record<string, MCPServerConfig> | undefined; | |
| private readonly mcpEnablementCallbacks?: McpEnablementCallbacks; | |
| private userMemory: string | HierarchicalMemory; | |
| private geminiMdFileCount: number; | |
| private geminiMdFilePaths: string[]; | |
| private readonly showMemoryUsage: boolean; | |
| private readonly logRagSnippets: boolean; | |
| private readonly accessibility: AccessibilitySettings; | |
| private readonly telemetrySettings: TelemetrySettings; | |
| private readonly usageStatisticsEnabled: boolean; | |
| geminiClient!: GeminiClient; | |
| sandboxManager: SandboxManager; | |
| private readonly _sandboxPolicyManager: SandboxPolicyManager; | |
| private baseLlmClient!: BaseLlmClient; | |
| private localLiteRtLmClient?: LocalLiteRtLmClient; | |
| private modelRouterService: ModelRouterService; | |
| private readonly modelAvailabilityService: ModelAvailabilityService; | |
| private readonly fileFiltering: { | |
| respectGitIgnore: boolean; | |
| respectGeminiIgnore: boolean; | |
| enableFileWatcher: boolean; | |
| enableRecursiveFileSearch: boolean; | |
| enableFuzzySearch: boolean; | |
| maxFileCount: number; | |
| searchTimeout: number; | |
| customIgnoreFilePaths: string[]; | |
| }; | |
| private fileDiscoveryService: FileDiscoveryService | null = null; | |
| private gitService: GitService | undefined = undefined; | |
| private readonly checkpointing: boolean; | |
| private readonly proxy: string | undefined; | |
| private readonly cwd: string; | |
| private readonly bugCommand: BugCommandSettings | undefined; | |
| private model: string; | |
| private readonly disableLoopDetection: boolean; | |
| // null = unknown (quota not fetched); true = has access; false = definitively no access | |
| private hasAccessToPreviewModel: boolean | null = null; | |
| private readonly noBrowser: boolean; | |
| private readonly folderTrust: boolean; | |
| private ideMode: boolean; | |
| private _activeModel: string; | |
| private fallbackOverrides = new Map<string, string>(); | |
| private readonly maxSessionTurns: number; | |
| private readonly listSessions: boolean; | |
| private readonly deleteSession: string | undefined; | |
| private readonly listExtensions: boolean; | |
| private readonly _extensionLoader: ExtensionLoader; | |
| private readonly _enabledExtensions: string[]; | |
| private readonly enableExtensionReloading: boolean; | |
| fallbackModelHandler?: FallbackModelHandler; | |
| validationHandler?: ValidationHandler; | |
| private quotaErrorOccurred: boolean = false; | |
| private creditsNotificationShown: boolean = false; | |
| private modelQuotas: Map< | |
| string, | |
| { remaining: number; limit: number; resetTime?: string } | |
| > = new Map(); | |
| private lastRetrievedQuota?: RetrieveUserQuotaResponse; | |
| private lastQuotaFetchTime = 0; | |
| private lastEmittedQuotaRemaining: number | undefined; | |
| private lastEmittedQuotaLimit: number | undefined; | |
| private emitQuotaChangedEvent(): void { | |
| const remaining = this.getQuotaRemaining(); | |
| const limit = this.getQuotaLimit(); | |
| const resetTime = this.getQuotaResetTime(); | |
| if ( | |
| this.lastEmittedQuotaRemaining !== remaining || | |
| this.lastEmittedQuotaLimit !== limit | |
| ) { | |
| this.lastEmittedQuotaRemaining = remaining; | |
| this.lastEmittedQuotaLimit = limit; | |
| coreEvents.emitQuotaChanged(remaining, limit, resetTime); | |
| } | |
| } | |
| private readonly summarizeToolOutput: | |
| | Record<string, SummarizeToolOutputSettings> | |
| | undefined; | |
| private readonly acpMode: boolean = false; | |
| private readonly loadMemoryFromIncludeDirectories: boolean = false; | |
| private readonly includeDirectoryTree: boolean = true; | |
| private readonly importFormat: 'tree' | 'flat'; | |
| private readonly discoveryMaxDirs: number; | |
| private readonly compressionThreshold: number | undefined; | |
| /** Public for testing only */ | |
| readonly interactive: boolean; | |
| private readonly ptyInfo: string; | |
| private readonly trustedFolder: boolean | undefined; | |
| private readonly directWebFetch: boolean; | |
| private readonly useRipgrep: boolean; | |
| private readonly enableInteractiveShell: boolean; | |
| private readonly shellBackgroundCompletionBehavior: | |
| | 'inject' | |
| | 'notify' | |
| | 'silent'; | |
| private readonly skipNextSpeakerCheck: boolean; | |
| private readonly useBackgroundColor: boolean; | |
| private readonly useAlternateBuffer: boolean; | |
| private readonly useTerminalBuffer: boolean; | |
| private readonly useRenderProcess: boolean; | |
| private shellExecutionConfig: ShellExecutionConfig; | |
| readonly env?: Record<string, string>; | |
| private readonly extensionManagement: boolean = true; | |
| private readonly extensionRegistryURI: string | undefined; | |
| private readonly truncateToolOutputThreshold: number; | |
| private compressionTruncationCounter = 0; | |
| private initialized = false; | |
| private initPromise: Promise<void> | undefined; | |
| private mcpInitializationPromise: Promise<void> | null = null; | |
| readonly storage: Storage; | |
| private readonly fileExclusions: FileExclusions; | |
| private readonly eventEmitter?: EventEmitter; | |
| private readonly useWriteTodos: boolean; | |
| private readonly workspacePoliciesDir: string | undefined; | |
| readonly messageBus: MessageBus; | |
| private readonly policyEngine: PolicyEngine; | |
| private policyUpdateConfirmationRequest: | |
| | PolicyUpdateConfirmationRequest | |
| | undefined; | |
| private readonly outputSettings: OutputSettings; | |
| private readonly gemmaModelRouter: GemmaModelRouterSettings; | |
| private readonly agentSessionNoninteractiveEnabled: boolean; | |
| private readonly agentSessionInteractiveEnabled: boolean; | |
| private readonly agentSessionSubagentEnabled: boolean; | |
| private readonly retryFetchErrors: boolean; | |
| private readonly maxAttempts: number; | |
| private readonly enableShellOutputEfficiency: boolean; | |
| private readonly shellToolInactivityTimeout: number; | |
| readonly fakeResponses?: string; | |
| readonly fakeResponsesNonStrict?: string; | |
| readonly recordResponses?: string; | |
| private readonly disableYoloMode: boolean; | |
| private readonly disableAlwaysAllow: boolean; | |
| private readonly rawOutput: boolean; | |
| private readonly acceptRawOutputRisk: boolean; | |
| private readonly dynamicModelConfiguration: boolean; | |
| private pendingIncludeDirectories: string[]; | |
| private readonly enableHooksUI: boolean; | |
| private readonly enableHooks: boolean; | |
| private hooks: { [K in HookEventName]?: HookDefinition[] } | undefined; | |
| private projectHooks: | |
| | ({ [K in HookEventName]?: HookDefinition[] } & { disabled?: string[] }) | |
| | undefined; | |
| private disabledHooks: string[]; | |
| private experiments: Experiments | undefined; | |
| private experimentsPromise: Promise<Experiments | undefined> | undefined; | |
| private hookSystem?: HookSystem; | |
| private readonly onModelChange: ((model: string) => void) | undefined; | |
| private readonly onReload: | |
| | (() => Promise<{ | |
| disabledSkills?: string[]; | |
| adminSkillsEnabled?: boolean; | |
| agents?: AgentSettings; | |
| }>) | |
| | undefined; | |
| private readonly billing: { | |
| overageStrategy: OverageStrategy; | |
| }; | |
| private readonly vertexAiRouting: VertexAiRoutingConfig | undefined; | |
| private readonly enableAgents: boolean; | |
| private agents: AgentSettings; | |
| private readonly enableEventDrivenScheduler: boolean; | |
| private readonly skillsSupport: boolean; | |
| private disabledSkills: string[]; | |
| private readonly adminSkillsEnabled: boolean; | |
| private readonly experimentalAutoMemory: boolean; | |
| private readonly experimentalGemma: boolean; | |
| private readonly experimentalContextManagementConfig?: string; | |
| private readonly memoryBoundaryMarkers: readonly string[]; | |
| private readonly topicUpdateNarration: boolean; | |
| private readonly disableLLMCorrection: boolean; | |
| private readonly planEnabled: boolean; | |
| private readonly voiceMode: boolean; | |
| private readonly trackerEnabled: boolean; | |
| private readonly planModeRoutingEnabled: boolean; | |
| private readonly modelSteering: boolean; | |
| private memoryContextManager?: MemoryContextManager; | |
| private readonly contextManagement: ContextManagementConfig; | |
| private terminalBackground: string | undefined = undefined; | |
| private remoteAdminSettings: AdminControlsSettings | undefined; | |
| private latestApiRequest: GenerateContentParameters | undefined; | |
| private lastModeSwitchTime: number = performance.now(); | |
| readonly injectionService: InjectionService; | |
| private approvedPlanPath: string | undefined; | |
| constructor(params: ConfigParameters) { | |
| this._sessionId = params.sessionId; | |
| this.promptId = params.sessionId; | |
| this.clientName = params.clientName; | |
| this._clientVersion = params.clientVersion ?? 'unknown'; | |
| this.approvedPlanPath = undefined; | |
| this.embeddingModel = | |
| params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL; | |
| this.sandbox = params.sandbox | |
| ? { | |
| enabled: params.sandbox.enabled || params.toolSandboxing || false, | |
| allowedPaths: params.sandbox.allowedPaths ?? [], | |
| includeDirectories: [ | |
| ...(params.sandbox.includeDirectories ?? []), | |
| ...(params.sandbox.allowedPaths ?? []), | |
| Storage.getGlobalTempDir(), | |
| ], | |
| networkAccess: params.sandbox.networkAccess ?? false, | |
| command: params.sandbox.command, | |
| image: params.sandbox.image, | |
| } | |
| : { | |
| enabled: params.toolSandboxing || false, | |
| allowedPaths: [], | |
| includeDirectories: [Storage.getGlobalTempDir()], | |
| networkAccess: false, | |
| }; | |
| this.targetDir = path.resolve(params.targetDir); | |
| this.folderTrust = params.folderTrust ?? false; | |
| this.workspaceContext = new WorkspaceContext(this.targetDir, []); | |
| this.pendingIncludeDirectories = params.includeDirectories ?? []; | |
| this.debugMode = params.debugMode; | |
| this.question = params.question; | |
| this.worktreeSettings = params.worktreeSettings; | |
| this._sandboxPolicyManager = new SandboxPolicyManager(); | |
| const initialApprovalMode = | |
| params.approvalMode ?? | |
| params.policyEngineConfig?.approvalMode ?? | |
| 'default'; | |
| this.sandboxManager = createSandboxManager( | |
| this.sandbox, | |
| { | |
| workspace: this.targetDir, | |
| forbiddenPaths: this.getSandboxForbiddenPaths.bind(this), | |
| includeDirectories: [ | |
| ...this.pendingIncludeDirectories, | |
| Storage.getGlobalTempDir(), | |
| ], | |
| policyManager: this._sandboxPolicyManager, | |
| }, | |
| initialApprovalMode, | |
| ); | |
| if ( | |
| !(this.sandboxManager instanceof NoopSandboxManager) && | |
| this.sandbox?.enabled | |
| ) { | |
| this.fileSystemService = new SandboxedFileSystemService( | |
| this.sandboxManager, | |
| params.targetDir, | |
| ); | |
| } else { | |
| this.fileSystemService = new StandardFileSystemService(); | |
| } | |
| this.debugMode = params.debugMode; | |
| this.question = params.question; | |
| this.worktreeSettings = params.worktreeSettings; | |
| this.coreTools = params.coreTools; | |
| this.mainAgentTools = params.mainAgentTools; | |
| this.allowedTools = params.allowedTools; | |
| this.excludeTools = params.excludeTools; | |
| this.toolDiscoveryCommand = params.toolDiscoveryCommand; | |
| this.toolCallCommand = params.toolCallCommand; | |
| this.mcpServerCommand = params.mcpServerCommand; | |
| this.mcpServers = params.mcpServers; | |
| this.mcpEnablementCallbacks = params.mcpEnablementCallbacks; | |
| this.mcpEnabled = params.mcpEnabled ?? true; | |
| this.extensionsEnabled = params.extensionsEnabled ?? true; | |
| this.allowedMcpServers = params.allowedMcpServers ?? []; | |
| this.blockedMcpServers = params.blockedMcpServers ?? []; | |
| this.allowedEnvironmentVariables = params.allowedEnvironmentVariables ?? []; | |
| this.blockedEnvironmentVariables = params.blockedEnvironmentVariables ?? []; | |
| this.enableEnvironmentVariableRedaction = | |
| params.enableEnvironmentVariableRedaction ?? false; | |
| this.userMemory = params.userMemory ?? ''; | |
| this.geminiMdFileCount = params.geminiMdFileCount ?? 0; | |
| this.geminiMdFilePaths = params.geminiMdFilePaths ?? []; | |
| this.showMemoryUsage = params.showMemoryUsage ?? false; | |
| this.logRagSnippets = params.logRagSnippets ?? false; | |
| this.accessibility = params.accessibility ?? {}; | |
| this.telemetrySettings = { | |
| enabled: params.telemetry?.enabled ?? false, | |
| traces: params.telemetry?.traces ?? false, | |
| target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET, | |
| otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT, | |
| otlpProtocol: params.telemetry?.otlpProtocol, | |
| logPrompts: params.telemetry?.logPrompts ?? true, | |
| outfile: params.telemetry?.outfile, | |
| useCollector: params.telemetry?.useCollector, | |
| useCliAuth: params.telemetry?.useCliAuth, | |
| }; | |
| this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true; | |
| this.fileFiltering = { | |
| respectGitIgnore: | |
| params.fileFiltering?.respectGitIgnore ?? | |
| DEFAULT_FILE_FILTERING_OPTIONS.respectGitIgnore, | |
| respectGeminiIgnore: | |
| params.fileFiltering?.respectGeminiIgnore ?? | |
| DEFAULT_FILE_FILTERING_OPTIONS.respectGeminiIgnore, | |
| enableFileWatcher: | |
| params.fileFiltering?.enableFileWatcher ?? | |
| DEFAULT_FILE_FILTERING_OPTIONS.enableFileWatcher ?? | |
| true, | |
| enableRecursiveFileSearch: | |
| params.fileFiltering?.enableRecursiveFileSearch ?? true, | |
| enableFuzzySearch: params.fileFiltering?.enableFuzzySearch ?? true, | |
| maxFileCount: | |
| params.fileFiltering?.maxFileCount ?? | |
| DEFAULT_FILE_FILTERING_OPTIONS.maxFileCount ?? | |
| 20000, | |
| searchTimeout: | |
| params.fileFiltering?.searchTimeout ?? | |
| DEFAULT_FILE_FILTERING_OPTIONS.searchTimeout ?? | |
| 5000, | |
| customIgnoreFilePaths: params.fileFiltering?.customIgnoreFilePaths ?? [], | |
| }; | |
| this.checkpointing = params.checkpointing ?? false; | |
| this.proxy = params.proxy; | |
| this.cwd = params.cwd ?? process.cwd(); | |
| this.env = params.env; | |
| this.fileDiscoveryService = params.fileDiscoveryService ?? null; | |
| this.bugCommand = params.bugCommand; | |
| this.model = params.model; | |
| this.disableLoopDetection = params.disableLoopDetection ?? false; | |
| this._activeModel = params.model; | |
| this.enableAgents = params.enableAgents ?? true; | |
| this.agents = params.agents ?? {}; | |
| this.disableLLMCorrection = params.disableLLMCorrection ?? true; | |
| this.planEnabled = params.plan ?? true; | |
| this.voiceMode = params.voiceMode ?? false; | |
| this.trackerEnabled = params.tracker ?? false; | |
| this.planModeRoutingEnabled = params.planSettings?.modelRouting ?? true; | |
| this.enableEventDrivenScheduler = params.enableEventDrivenScheduler ?? true; | |
| this.skillsSupport = params.skillsSupport ?? true; | |
| this.disabledSkills = params.disabledSkills ?? []; | |
| this.adminSkillsEnabled = params.adminSkillsEnabled ?? true; | |
| this.modelAvailabilityService = new ModelAvailabilityService(); | |
| this.dynamicModelConfiguration = params.dynamicModelConfiguration ?? false; | |
| // HACK: The settings loading logic doesn't currently merge the default | |
| // generation config with the user's settings. This means if a user provides | |
| // any `generation` settings (e.g., just `overrides`), the default `aliases` | |
| // are lost. This hack manually merges the default aliases back in if they | |
| // are missing from the user's config. | |
| // TODO(12593): Fix the settings loading logic to properly merge defaults and | |
| // remove this hack. | |
| let modelConfigServiceConfig = params.modelConfigServiceConfig; | |
| if (modelConfigServiceConfig) { | |
| // Ensure user-defined model definitions augment, not replace, the defaults. | |
| const mergedModelDefinitions = { | |
| ...DEFAULT_MODEL_CONFIGS.modelDefinitions, | |
| ...modelConfigServiceConfig.modelDefinitions, | |
| }; | |
| const mergedModelIdResolutions = { | |
| ...DEFAULT_MODEL_CONFIGS.modelIdResolutions, | |
| ...modelConfigServiceConfig.modelIdResolutions, | |
| }; | |
| const mergedClassifierIdResolutions = { | |
| ...DEFAULT_MODEL_CONFIGS.classifierIdResolutions, | |
| ...modelConfigServiceConfig.classifierIdResolutions, | |
| }; | |
| const mergedModelChains = { | |
| ...DEFAULT_MODEL_CONFIGS.modelChains, | |
| ...modelConfigServiceConfig.modelChains, | |
| }; | |
| modelConfigServiceConfig = { | |
| // Preserve other user settings like customAliases | |
| ...modelConfigServiceConfig, | |
| // Apply defaults for aliases and overrides if they are not provided | |
| aliases: | |
| modelConfigServiceConfig.aliases ?? DEFAULT_MODEL_CONFIGS.aliases, | |
| overrides: | |
| modelConfigServiceConfig.overrides ?? DEFAULT_MODEL_CONFIGS.overrides, | |
| // Use the merged model definitions | |
| modelDefinitions: mergedModelDefinitions, | |
| modelIdResolutions: mergedModelIdResolutions, | |
| classifierIdResolutions: mergedClassifierIdResolutions, | |
| modelChains: mergedModelChains, | |
| }; | |
| } | |
| this.modelConfigService = new ModelConfigService( | |
| modelConfigServiceConfig ?? DEFAULT_MODEL_CONFIGS, | |
| ); | |
| this.experimentalAutoMemory = params.experimentalAutoMemory ?? false; | |
| this.experimentalGemma = params.experimentalGemma ?? true; | |
| this.experimentalContextManagementConfig = | |
| params.experimentalContextManagementConfig; | |
| this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git']; | |
| this.contextManagement = { | |
| enabled: params.contextManagement?.enabled ?? false, | |
| historyWindow: { | |
| maxTokens: params.contextManagement?.historyWindow?.maxTokens ?? 150000, | |
| retainedTokens: | |
| params.contextManagement?.historyWindow?.retainedTokens ?? 40000, | |
| }, | |
| messageLimits: { | |
| normalMaxTokens: | |
| params.contextManagement?.messageLimits?.normalMaxTokens ?? 2500, | |
| retainedMaxTokens: | |
| params.contextManagement?.messageLimits?.retainedMaxTokens ?? 12000, | |
| normalizationHeadRatio: | |
| params.contextManagement?.messageLimits?.normalizationHeadRatio ?? | |
| 0.25, | |
| }, | |
| tools: { | |
| distillation: { | |
| maxOutputTokens: | |
| params.contextManagement?.tools?.distillation?.maxOutputTokens ?? | |
| 10000, | |
| summarizationThresholdTokens: | |
| params.contextManagement?.tools?.distillation | |
| ?.summarizationThresholdTokens ?? 20000, | |
| }, | |
| outputMasking: { | |
| protectionThresholdTokens: | |
| params.contextManagement?.tools?.outputMasking | |
| ?.protectionThresholdTokens ?? DEFAULT_TOOL_PROTECTION_THRESHOLD, | |
| minPrunableThresholdTokens: | |
| params.contextManagement?.tools?.outputMasking | |
| ?.minPrunableThresholdTokens ?? | |
| DEFAULT_MIN_PRUNABLE_TOKENS_THRESHOLD, | |
| protectLatestTurn: | |
| params.contextManagement?.tools?.outputMasking?.protectLatestTurn ?? | |
| DEFAULT_PROTECT_LATEST_TURN, | |
| }, | |
| }, | |
| }; | |
| this.topicUpdateNarration = params.topicUpdateNarration ?? true; | |
| this.modelSteering = params.modelSteering ?? false; | |
| this.injectionService = new InjectionService(() => | |
| this.isModelSteeringEnabled(), | |
| ); | |
| ExecutionLifecycleService.setInjectionService(this.injectionService); | |
| this.maxSessionTurns = params.maxSessionTurns ?? -1; | |
| this.acpMode = params.acpMode ?? false; | |
| this.listSessions = params.listSessions ?? false; | |
| this.deleteSession = params.deleteSession; | |
| this.listExtensions = params.listExtensions ?? false; | |
| this._extensionLoader = | |
| params.extensionLoader ?? new SimpleExtensionLoader([]); | |
| this._enabledExtensions = params.enabledExtensions ?? []; | |
| this.noBrowser = params.noBrowser ?? false; | |
| this.summarizeToolOutput = params.summarizeToolOutput; | |
| this.folderTrust = params.folderTrust ?? false; | |
| this.ideMode = params.ideMode ?? false; | |
| this.includeDirectoryTree = params.includeDirectoryTree ?? true; | |
| this.loadMemoryFromIncludeDirectories = | |
| params.loadMemoryFromIncludeDirectories ?? false; | |
| this.importFormat = params.importFormat ?? 'tree'; | |
| this.discoveryMaxDirs = params.discoveryMaxDirs ?? 200; | |
| this.compressionThreshold = params.compressionThreshold; | |
| this.interactive = params.interactive ?? false; | |
| this.ptyInfo = params.ptyInfo ?? 'child_process'; | |
| this.trustedFolder = params.trustedFolder; | |
| this.directWebFetch = params.directWebFetch ?? false; | |
| this.useRipgrep = params.useRipgrep ?? true; | |
| this.useBackgroundColor = params.useBackgroundColor ?? true; | |
| this.useAlternateBuffer = params.useAlternateBuffer ?? false; | |
| this.useTerminalBuffer = params.useTerminalBuffer ?? false; | |
| this.useRenderProcess = params.useRenderProcess ?? true; | |
| this.enableInteractiveShell = params.enableInteractiveShell ?? false; | |
| const requestedBehavior = params.shellBackgroundCompletionBehavior; | |
| if (requestedBehavior === 'inject' || requestedBehavior === 'notify') { | |
| this.shellBackgroundCompletionBehavior = requestedBehavior; | |
| } else { | |
| this.shellBackgroundCompletionBehavior = 'silent'; | |
| } | |
| this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true; | |
| this.shellExecutionConfig = { | |
| terminalWidth: params.shellExecutionConfig?.terminalWidth ?? 80, | |
| terminalHeight: params.shellExecutionConfig?.terminalHeight ?? 24, | |
| showColor: params.shellExecutionConfig?.showColor ?? false, | |
| pager: params.shellExecutionConfig?.pager ?? 'cat', | |
| sanitizationConfig: this.sanitizationConfig, | |
| sandboxManager: this.sandboxManager, | |
| sandboxConfig: this.sandbox, | |
| backgroundCompletionBehavior: this.shellBackgroundCompletionBehavior, | |
| }; | |
| this.truncateToolOutputThreshold = | |
| params.truncateToolOutputThreshold ?? | |
| DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD; | |
| const isGemini2 = isGemini2Model(this.model); | |
| this.useWriteTodos = | |
| isGemini2 && !isPreviewModel(this.model, this) && !this.trackerEnabled | |
| ? (params.useWriteTodos ?? true) | |
| : false; | |
| this.workspacePoliciesDir = params.workspacePoliciesDir; | |
| this.enableHooksUI = params.enableHooksUI ?? true; | |
| this.enableHooks = params.enableHooks ?? true; | |
| this.disabledHooks = params.disabledHooks ?? []; | |
| this.enableShellOutputEfficiency = | |
| params.enableShellOutputEfficiency ?? true; | |
| this.shellToolInactivityTimeout = | |
| (params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes | |
| this.extensionManagement = params.extensionManagement ?? true; | |
| this.extensionRegistryURI = params.extensionRegistryURI; | |
| this.enableExtensionReloading = params.enableExtensionReloading ?? false; | |
| this.storage = new Storage(this.targetDir, this._sessionId); | |
| this.storage.setCustomPlansDir(params.planSettings?.directory); | |
| this.fakeResponses = params.fakeResponses; | |
| this.fakeResponsesNonStrict = params.fakeResponsesNonStrict; | |
| this.recordResponses = params.recordResponses; | |
| this.fileExclusions = new FileExclusions(this); | |
| this.eventEmitter = params.eventEmitter; | |
| this.enableConseca = params.enableConseca ?? false; | |
| // Initialize Safety Infrastructure | |
| const contextBuilder = new ContextBuilder(this); | |
| const checkersPath = this.targetDir; | |
| // The checkersPath is used to resolve external checkers. Since we do not have any external checkers currently, it is set to the targetDir. | |
| const checkerRegistry = new CheckerRegistry(checkersPath); | |
| const checkerRunner = new CheckerRunner(contextBuilder, checkerRegistry, { | |
| checkersPath, | |
| timeout: 30000, // 30 seconds to allow for LLM-based checkers | |
| }); | |
| this.policyUpdateConfirmationRequest = | |
| params.policyUpdateConfirmationRequest; | |
| this.disableAlwaysAllow = params.disableAlwaysAllow ?? false; | |
| const engineApprovalMode = | |
| params.approvalMode ?? | |
| params.policyEngineConfig?.approvalMode ?? | |
| ApprovalMode.DEFAULT; | |
| this.policyEngine = new PolicyEngine( | |
| { | |
| ...params.policyEngineConfig, | |
| approvalMode: engineApprovalMode, | |
| disableAlwaysAllow: this.disableAlwaysAllow, | |
| sandboxManager: this.sandboxManager, | |
| isTrustedFolder: () => this.isTrustedFolder(), | |
| }, | |
| checkerRunner, | |
| ); | |
| // Register Conseca if enabled | |
| if (this.enableConseca) { | |
| debugLogger.log('[SAFETY] Registering Conseca Safety Checker'); | |
| ConsecaSafetyChecker.getInstance().setContext(this); | |
| } | |
| this.messageBus = new MessageBus(this.policyEngine, this.debugMode); | |
| this.acknowledgedAgentsService = new AcknowledgedAgentsService(); | |
| this.skillManager = new SkillManager(); | |
| this.outputSettings = { | |
| format: params.output?.format ?? OutputFormat.TEXT, | |
| }; | |
| this.gemmaModelRouter = { | |
| enabled: params.gemmaModelRouter?.enabled ?? false, | |
| autoStartServer: params.gemmaModelRouter?.autoStartServer ?? true, | |
| binaryPath: params.gemmaModelRouter?.binaryPath ?? '', | |
| classifier: { | |
| host: | |
| params.gemmaModelRouter?.classifier?.host ?? 'http://localhost:9379', | |
| model: | |
| params.gemmaModelRouter?.classifier?.model ?? 'gemma3-1b-gpu-custom', | |
| }, | |
| }; | |
| this.agentSessionNoninteractiveEnabled = | |
| params.adk?.agentSessionNoninteractiveEnabled ?? false; | |
| this.agentSessionInteractiveEnabled = | |
| params.adk?.agentSessionInteractiveEnabled ?? false; | |
| this.agentSessionSubagentEnabled = | |
| params.adk?.agentSessionSubagentEnabled ?? false; | |
| this.retryFetchErrors = params.retryFetchErrors ?? true; | |
| this.maxAttempts = Math.min( | |
| params.maxAttempts ?? DEFAULT_MAX_ATTEMPTS, | |
| DEFAULT_MAX_ATTEMPTS, | |
| ); | |
| this.disableYoloMode = params.disableYoloMode ?? false; | |
| this.rawOutput = params.rawOutput ?? false; | |
| this.acceptRawOutputRisk = params.acceptRawOutputRisk ?? false; | |
| if (params.hooks) { | |
| this.hooks = params.hooks; | |
| } | |
| if (params.projectHooks) { | |
| this.projectHooks = params.projectHooks; | |
| } | |
| this.experiments = params.experiments; | |
| this.onModelChange = params.onModelChange; | |
| this.onReload = params.onReload; | |
| this.billing = { | |
| overageStrategy: params.billing?.overageStrategy ?? 'ask', | |
| }; | |
| this.vertexAiRouting = params.vertexAiRouting; | |
| if (params.contextFileName) { | |
| setGeminiMdFilename(params.contextFileName); | |
| } | |
| if (this.telemetrySettings.enabled) { | |
| // eslint-disable-next-line @typescript-eslint/no-floating-promises | |
| initializeTelemetry(this); | |
| } | |
| const proxy = this.getProxy(); | |
| if (proxy) { | |
| try { | |
| setGlobalProxy(proxy); | |
| } catch (error) { | |
| coreEvents.emitFeedback( | |
| 'error', | |
| 'Invalid proxy configuration detected. Check debug drawer for more details (F12)', | |
| error, | |
| ); | |
| } | |
| } | |
| this.geminiClient = new GeminiClient(this); | |
| this.a2aClientManager = new A2AClientManager(this); | |
| this.modelRouterService = new ModelRouterService(this); | |
| } | |
| isInitialized(): boolean { | |
| return this.initialized; | |
| } | |
| /** | |
| * Dedups initialization requests using a shared promise that is only resolved | |
| * once. | |
| */ | |
| async initialize(): Promise<void> { | |
| if (this.initPromise) { | |
| return this.initPromise; | |
| } | |
| this.initPromise = this._initialize(); | |
| return this.initPromise; | |
| } | |
| private async _initialize(): Promise<void> { | |
| await this.storage.initialize(); | |
| ragLogger.initialize(this.storage.getProjectTempLogsDir()); | |
| // Add pending directories to workspace context | |
| for (const dir of this.pendingIncludeDirectories) { | |
| this.workspaceContext.addDirectory(dir); | |
| } | |
| // Add plans directory to workspace context for plan file storage | |
| if (this.planEnabled) { | |
| let plansDir: string; | |
| try { | |
| plansDir = this.storage.getPlansDir(); | |
| } catch (error) { | |
| // Fallback to the default plan dir if any error occurs | |
| const errorMessage = | |
| error instanceof Error ? error.message : String(error); | |
| coreEvents.emitFeedback( | |
| 'warning', | |
| 'Invalid custom plans directory: ' + | |
| errorMessage + | |
| '. Falling back to default project temp directory.', | |
| error, | |
| ); | |
| this.storage.setCustomPlansDir(undefined); | |
| plansDir = this.storage.getPlansDir(); | |
| } | |
| try { | |
| await fs.promises.access(plansDir); | |
| this.workspaceContext.addDirectory(plansDir); | |
| } catch { | |
| // Directory does not exist yet, so we don't add it to the workspace context. | |
| // It will be created when the first plan is written. Since custom plan | |
| // directories must be within the project root, they are automatically | |
| // covered by the project-wide file discovery once created. | |
| } | |
| } | |
| // Initialize centralized FileDiscoveryService | |
| const discoverToolsHandle = startupProfiler.start('discover_tools'); | |
| this.getFileService(); | |
| if (this.getCheckpointingEnabled()) { | |
| await this.getGitService(); | |
| } | |
| this.promptRegistry = new PromptRegistry(); | |
| this.resourceRegistry = new ResourceRegistry(); | |
| this.agentRegistry = new AgentRegistry(this); | |
| await this.agentRegistry.initialize(); | |
| coreEvents.on(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed); | |
| this.toolRegistry = await this.createToolRegistry(); | |
| discoverToolsHandle?.end(); | |
| this.mcpClientManager = new McpClientManager( | |
| this.clientVersion, | |
| this, | |
| this.eventEmitter, | |
| ); | |
| this.mcpClientManager.setMainRegistries({ | |
| toolRegistry: this.toolRegistry, | |
| promptRegistry: this.promptRegistry, | |
| resourceRegistry: this.resourceRegistry, | |
| }); | |
| // We do not await this promise so that the CLI can start up even if | |
| // MCP servers are slow to connect. | |
| this.mcpInitializationPromise = Promise.allSettled([ | |
| this.mcpClientManager.startConfiguredMcpServers(), | |
| this.getExtensionLoader().start(this), | |
| ]).then((results) => { | |
| for (const result of results) { | |
| if (result.status === 'rejected') { | |
| debugLogger.error('Error initializing MCP clients:', result.reason); | |
| } | |
| } | |
| }); | |
| if (!this.interactive || this.acpMode) { | |
| await this.mcpInitializationPromise; | |
| } | |
| if (this.skillsSupport) { | |
| this.getSkillManager().setAdminSettings(this.adminSkillsEnabled); | |
| if (this.adminSkillsEnabled) { | |
| await this.getSkillManager().discoverSkills( | |
| this.storage, | |
| this.getExtensions(), | |
| this.isTrustedFolder(), | |
| ); | |
| this.getSkillManager().setDisabledSkills(this.disabledSkills); | |
| // Re-register ActivateSkillTool to update its schema with the discovered enabled skill enums | |
| if (this.getSkillManager().getSkills().length > 0) { | |
| this.toolRegistry.unregisterTool(ActivateSkillTool.Name); | |
| this.toolRegistry.registerTool( | |
| new ActivateSkillTool(this, this.messageBus), | |
| ); | |
| } | |
| } | |
| } | |
| // Initialize hook system if enabled | |
| if (this.getEnableHooks()) { | |
| this.hookSystem = new HookSystem(this); | |
| await this.hookSystem.initialize(); | |
| } | |
| this.memoryContextManager = new MemoryContextManager(this); | |
| await this.memoryContextManager.refresh(); | |
| await this.geminiClient.initialize(); | |
| this.initialized = true; | |
| } | |
| getContentGenerator(): ContentGenerator { | |
| return this.contentGenerator; | |
| } | |
| async refreshAuth( | |
| authMethod: AuthType, | |
| apiKey?: string, | |
| baseUrl?: string, | |
| customHeaders?: Record<string, string>, | |
| ) { | |
| // Reset availability service when switching auth | |
| this.modelAvailabilityService.reset(); | |
| this.fallbackOverrides.clear(); | |
| this.modelConfigService.clearRuntimeOverrides(); | |
| // Vertex and Genai have incompatible encryption and sending history with | |
| // thoughtSignature from Genai to Vertex will fail, we need to strip them | |
| if ( | |
| this.contentGeneratorConfig?.authType === AuthType.USE_GEMINI && | |
| authMethod !== AuthType.USE_GEMINI | |
| ) { | |
| // Restore the conversation history to the new client | |
| this.geminiClient.stripThoughtsFromHistory(); | |
| } | |
| // Reset availability status when switching auth (e.g. from limited key to OAuth) | |
| this.modelAvailabilityService.reset(); | |
| // Clear stale authType to ensure getGemini31LaunchedSync doesn't return stale results | |
| // during the transition. | |
| if (this.contentGeneratorConfig) { | |
| this.contentGeneratorConfig.authType = undefined; | |
| } | |
| const newContentGeneratorConfig = await createContentGeneratorConfig( | |
| this, | |
| authMethod, | |
| apiKey, | |
| baseUrl, | |
| customHeaders, | |
| this.vertexAiRouting, | |
| ); | |
| this.contentGenerator = await createContentGenerator( | |
| newContentGeneratorConfig, | |
| this, | |
| this.getSessionId(), | |
| ); | |
| // Only assign to instance properties after successful initialization | |
| this.contentGeneratorConfig = newContentGeneratorConfig; | |
| const codeAssistServer = getCodeAssistServer(this); | |
| const quotaPromise = codeAssistServer?.projectId | |
| ? this.refreshUserQuota() | |
| : Promise.resolve(); | |
| this.experimentsPromise = getExperiments(codeAssistServer) | |
| .then((experiments) => { | |
| this.setExperiments(experiments); | |
| return experiments; | |
| }) | |
| .catch((e) => { | |
| debugLogger.error('Failed to fetch experiments', e); | |
| return undefined; | |
| }); | |
| const [experiments] = await Promise.all([ | |
| this.experimentsPromise, | |
| quotaPromise.catch((e) => { | |
| debugLogger.error('Failed to fetch user quota', e); | |
| }), | |
| ]); | |
| const requestTimeoutMs = this.getRequestTimeoutMs(); | |
| if (requestTimeoutMs !== undefined) { | |
| updateGlobalFetchTimeouts(requestTimeoutMs); | |
| } | |
| // Initialize BaseLlmClient now that the ContentGenerator and experiments are available | |
| this.baseLlmClient = new BaseLlmClient(this.contentGenerator, this); | |
| const authType = this.contentGeneratorConfig.authType; | |
| if ( | |
| authType === AuthType.USE_GEMINI || | |
| authType === AuthType.USE_VERTEX_AI | |
| ) { | |
| this.setHasAccessToPreviewModel(true); | |
| } | |
| // Only reset when we have explicit "no access" (hasAccessToPreviewModel === false). | |
| // When null (quota not fetched) or true, we preserve the saved model. | |
| if ( | |
| isPreviewModel(this.model, this) && | |
| this.hasAccessToPreviewModel === false | |
| ) { | |
| this.setModel(DEFAULT_GEMINI_MODEL_AUTO); | |
| } | |
| const adminControlsEnabled = | |
| experiments?.flags[ExperimentFlags.ENABLE_ADMIN_CONTROLS]?.boolValue ?? | |
| false; | |
| try { | |
| const adminControls = await fetchAdminControls( | |
| codeAssistServer, | |
| this.getRemoteAdminSettings(), | |
| adminControlsEnabled, | |
| (newSettings: AdminControlsSettings) => { | |
| this.setRemoteAdminSettings(newSettings); | |
| coreEvents.emitAdminSettingsChanged(); | |
| }, | |
| ); | |
| this.setRemoteAdminSettings(adminControls); | |
| } catch (e) { | |
| debugLogger.error('Failed to fetch admin controls', e); | |
| } | |
| if ((await this.getProModelNoAccess()) && isAutoModel(this.model)) { | |
| this.setModel(PREVIEW_GEMINI_FLASH_MODEL); | |
| } | |
| } | |
| async getExperimentsAsync(): Promise<Experiments | undefined> { | |
| if (this.experiments) { | |
| return this.experiments; | |
| } | |
| const codeAssistServer = getCodeAssistServer(this); | |
| return getExperiments(codeAssistServer); | |
| } | |
| getUserTier(): UserTierId | undefined { | |
| return this.contentGenerator?.userTier; | |
| } | |
| getUserTierName(): string | undefined { | |
| return this.contentGenerator?.userTierName; | |
| } | |
| getUserPaidTier(): GeminiUserTier | undefined { | |
| return this.contentGenerator?.paidTier; | |
| } | |
| /** | |
| * Provides access to the BaseLlmClient for stateless LLM operations. | |
| */ | |
| getBaseLlmClient(): BaseLlmClient { | |
| if (!this.baseLlmClient) { | |
| // Handle cases where initialization might be deferred or authentication failed | |
| if (!this.experiments) { | |
| throw new Error( | |
| 'BaseLlmClient not initialized. Ensure experiments have been fetched and configuration is ready.', | |
| ); | |
| } | |
| if (this.contentGenerator) { | |
| this.baseLlmClient = new BaseLlmClient( | |
| this.getContentGenerator(), | |
| this, | |
| ); | |
| } else { | |
| throw new Error( | |
| 'BaseLlmClient not initialized. Ensure authentication has occurred and ContentGenerator is ready.', | |
| ); | |
| } | |
| } | |
| return this.baseLlmClient; | |
| } | |
| getLocalLiteRtLmClient(): LocalLiteRtLmClient { | |
| if (!this.localLiteRtLmClient) { | |
| this.localLiteRtLmClient = new LocalLiteRtLmClient(this); | |
| } | |
| return this.localLiteRtLmClient; | |
| } | |
| /** @internal Backing field alias for backwards-compatibility */ | |
| get _toolRegistry(): ToolRegistry { | |
| return this.toolRegistry; | |
| } | |
| set _toolRegistry(value: ToolRegistry) { | |
| this.toolRegistry = value; | |
| } | |
| /** @internal Backing field alias for backwards-compatibility */ | |
| get _promptRegistry(): PromptRegistry { | |
| return this.promptRegistry; | |
| } | |
| set _promptRegistry(registry: PromptRegistry) { | |
| this.promptRegistry = registry; | |
| } | |
| /** @internal Backing field alias for backwards-compatibility */ | |
| get _resourceRegistry(): ResourceRegistry { | |
| return this.resourceRegistry; | |
| } | |
| set _resourceRegistry(registry: ResourceRegistry) { | |
| this.resourceRegistry = registry; | |
| } | |
| /** @internal Backing field alias for backwards-compatibility */ | |
| get _messageBus(): MessageBus { | |
| return this.messageBus; | |
| } | |
| /** @internal Backing field alias for backwards-compatibility */ | |
| get _geminiClient(): GeminiClient { | |
| return this.geminiClient; | |
| } | |
| set _geminiClient(client: GeminiClient) { | |
| this.geminiClient = client; | |
| } | |
| private async getSandboxForbiddenPaths(): Promise<string[]> { | |
| if (this._sandboxForbiddenPaths) { | |
| return this._sandboxForbiddenPaths; | |
| } | |
| this._sandboxForbiddenPaths = await this.getFileService().getIgnoredPaths({ | |
| respectGitIgnore: false, | |
| respectGeminiIgnore: true, | |
| }); | |
| return this._sandboxForbiddenPaths; | |
| } | |
| private refreshSandboxManager(): void { | |
| this.sandboxManager = createSandboxManager( | |
| this.sandbox, | |
| { | |
| workspace: this.targetDir, | |
| forbiddenPaths: this.getSandboxForbiddenPaths.bind(this), | |
| includeDirectories: [ | |
| ...this.pendingIncludeDirectories, | |
| Storage.getGlobalTempDir(), | |
| ], | |
| policyManager: this._sandboxPolicyManager, | |
| }, | |
| this.getApprovalMode(), | |
| ); | |
| this.shellExecutionConfig.sandboxManager = this.sandboxManager; | |
| } | |
| get sandboxPolicyManager() { | |
| return this._sandboxPolicyManager; | |
| } | |
| /** @internal Backing field alias for backwards-compatibility */ | |
| get _sandboxManager(): SandboxManager { | |
| return this.sandboxManager; | |
| } | |
| set _sandboxManager(manager: SandboxManager) { | |
| this.sandboxManager = manager; | |
| } | |
| getSessionId(): string { | |
| return this.promptId; | |
| } | |
| getWorktreeSettings(): WorktreeSettings | undefined { | |
| return this.worktreeSettings; | |
| } | |
| getClientName(): string | undefined { | |
| return this.clientName; | |
| } | |
| setSessionId(sessionId: string): void { | |
| const previousPlansDir = this.storage.isInitialized() | |
| ? this.storage.getPlansDir() | |
| : undefined; | |
| this._sessionId = sessionId; | |
| this.promptId = sessionId; | |
| this.storage.setSessionId(sessionId); | |
| this.trackerService = undefined; | |
| this.fallbackOverrides.clear(); | |
| this.modelConfigService.clearRuntimeOverrides(); | |
| this.approvedPlanPath = undefined; | |
| this.topicState.reset(); | |
| this.skillManager.reset(); | |
| this.latestApiRequest = undefined; | |
| this.lastModeSwitchTime = performance.now(); | |
| this.compressionTruncationCounter = 0; | |
| this.quotaErrorOccurred = false; | |
| this.creditsNotificationShown = false; | |
| this.modelAvailabilityService.reset(); | |
| this.modelQuotas.clear(); | |
| this.lastRetrievedQuota = undefined; | |
| this.lastQuotaFetchTime = 0; | |
| // Force an event emission to clear the UI display | |
| coreEvents.emitQuotaChanged(undefined, undefined, undefined); | |
| this.lastEmittedQuotaRemaining = undefined; | |
| this.lastEmittedQuotaLimit = undefined; | |
| if (previousPlansDir) { | |
| this.refreshSessionScopedPlansDirectory(previousPlansDir); | |
| } | |
| } | |
| rotateSessionId(sessionId: string): void { | |
| this._sessionId = sessionId; | |
| this.promptId = sessionId; | |
| } | |
| resetNewSessionState(sessionId: string): void { | |
| this.setSessionId(sessionId); | |
| } | |
| setTerminalBackground(terminalBackground: string | undefined): void { | |
| this.terminalBackground = terminalBackground; | |
| } | |
| getTerminalBackground(): string | undefined { | |
| return this.terminalBackground; | |
| } | |
| getLatestApiRequest(): GenerateContentParameters | undefined { | |
| return this.latestApiRequest; | |
| } | |
| setLatestApiRequest(req: GenerateContentParameters): void { | |
| this.latestApiRequest = req; | |
| } | |
| getRemoteAdminSettings(): AdminControlsSettings | undefined { | |
| return this.remoteAdminSettings; | |
| } | |
| setRemoteAdminSettings(settings: AdminControlsSettings | undefined): void { | |
| this.remoteAdminSettings = settings; | |
| } | |
| shouldLoadMemoryFromIncludeDirectories(): boolean { | |
| return this.loadMemoryFromIncludeDirectories; | |
| } | |
| getIncludeDirectoryTree(): boolean { | |
| return this.includeDirectoryTree; | |
| } | |
| getImportFormat(): 'tree' | 'flat' { | |
| return this.importFormat; | |
| } | |
| getDiscoveryMaxDirs(): number { | |
| return this.discoveryMaxDirs; | |
| } | |
| getContentGeneratorConfig(): ContentGeneratorConfig { | |
| return this.contentGeneratorConfig; | |
| } | |
| getModel(): string { | |
| return this.model; | |
| } | |
| getDisableLoopDetection(): boolean { | |
| return this.disableLoopDetection ?? false; | |
| } | |
| setModel(newModel: string, isTemporary: boolean = true): void { | |
| if (this.model !== newModel || this._activeModel !== newModel) { | |
| this.model = newModel; | |
| // When the user explicitly sets a model, that becomes the active model. | |
| this._activeModel = newModel; | |
| coreEvents.emitModelChanged(newModel); | |
| this.lastEmittedQuotaRemaining = undefined; | |
| this.lastEmittedQuotaLimit = undefined; | |
| this.emitQuotaChangedEvent(); | |
| } | |
| if (this.onModelChange && !isTemporary) { | |
| this.onModelChange(newModel); | |
| } | |
| this.modelAvailabilityService.reset(); | |
| } | |
| activateFallbackMode(model: string, failedModel?: string): void { | |
| debugLogger.log( | |
| `Model fallback activated: switching from ${failedModel ?? 'unknown'} to ${model}`, | |
| ); | |
| if (this.getActiveModel() !== model) { | |
| this.setModel(model, true); | |
| } | |
| if (failedModel) { | |
| // Chained fallback mitigation: If we already have overrides that point to the model | |
| // that just failed, we need to update them to point to the new fallback model. | |
| // e.g. A -> B, then B fails and we fallback to C. We must update A to point to C. | |
| for (const [source, target] of this.fallbackOverrides.entries()) { | |
| if (target === failedModel) { | |
| this.fallbackOverrides.set(source, model); | |
| this.modelConfigService.registerRuntimeModelOverride({ | |
| match: { model: source }, | |
| modelConfig: { model }, | |
| }); | |
| } | |
| } | |
| this.fallbackOverrides.set(failedModel, model); | |
| this.modelConfigService.registerRuntimeModelOverride({ | |
| match: { model: failedModel }, | |
| modelConfig: { model }, | |
| }); | |
| } | |
| const authType = this.getContentGeneratorConfig()?.authType; | |
| if (authType) { | |
| logFlashFallback(this, new FlashFallbackEvent(authType)); | |
| } | |
| } | |
| getFallbackOverride(model: string): string | undefined { | |
| return this.fallbackOverrides.get(model); | |
| } | |
| getActiveModel(): string { | |
| return this._activeModel ?? this.model; | |
| } | |
| setActiveModel(model: string): void { | |
| if (this._activeModel !== model) { | |
| this._activeModel = model; | |
| } | |
| } | |
| setFallbackModelHandler(handler: FallbackModelHandler): void { | |
| this.fallbackModelHandler = handler; | |
| } | |
| getFallbackModelHandler(): FallbackModelHandler | undefined { | |
| return this.fallbackModelHandler; | |
| } | |
| setValidationHandler(handler: ValidationHandler): void { | |
| this.validationHandler = handler; | |
| } | |
| getValidationHandler(): ValidationHandler | undefined { | |
| return this.validationHandler; | |
| } | |
| resetTurn(): void { | |
| this.modelAvailabilityService.resetTurn(); | |
| } | |
| /** Resets billing state (overageStrategy, creditsNotificationShown) once per user prompt. */ | |
| resetBillingTurnState(overageStrategy?: OverageStrategy): void { | |
| this.creditsNotificationShown = false; | |
| this.billing.overageStrategy = overageStrategy ?? 'ask'; | |
| } | |
| getMaxSessionTurns(): number { | |
| return this.maxSessionTurns; | |
| } | |
| setQuotaErrorOccurred(value: boolean): void { | |
| this.quotaErrorOccurred = value; | |
| } | |
| getQuotaErrorOccurred(): boolean { | |
| return this.quotaErrorOccurred; | |
| } | |
| setCreditsNotificationShown(value: boolean): void { | |
| this.creditsNotificationShown = value; | |
| } | |
| getCreditsNotificationShown(): boolean { | |
| return this.creditsNotificationShown; | |
| } | |
| setQuota( | |
| remaining: number | undefined, | |
| limit: number | undefined, | |
| modelId?: string, | |
| ): void { | |
| const activeModel = modelId ?? this.getActiveModel(); | |
| if (remaining !== undefined && limit !== undefined) { | |
| const current = this.modelQuotas.get(activeModel); | |
| if ( | |
| !current || | |
| current.remaining !== remaining || | |
| current.limit !== limit | |
| ) { | |
| this.modelQuotas.set(activeModel, { remaining, limit }); | |
| this.emitQuotaChangedEvent(); | |
| } | |
| } | |
| } | |
| private getPooledQuota(): { | |
| remaining?: number; | |
| limit?: number; | |
| resetTime?: string; | |
| } { | |
| const model = this.getModel(); | |
| if (!isAutoModel(model, this)) { | |
| return {}; | |
| } | |
| const primaryModel = resolveModel( | |
| model, | |
| this.getGemini31LaunchedSync(), | |
| this.getUseCustomToolModelSync(), | |
| this.getHasAccessToPreviewModel(), | |
| this, | |
| this.hasGemini35FlashGAAccess(), | |
| ); | |
| const isPreview = isPreviewModel(primaryModel, this); | |
| const proModel = primaryModel; | |
| const flashModel = isPreview | |
| ? PREVIEW_GEMINI_FLASH_MODEL | |
| : DEFAULT_GEMINI_FLASH_MODEL; | |
| const proQuota = this.modelQuotas.get(proModel); | |
| const flashQuota = this.modelQuotas.get(flashModel); | |
| if (proQuota || flashQuota) { | |
| // For reset time, take the one that is furthest in the future (most conservative) | |
| const resetTime = [proQuota?.resetTime, flashQuota?.resetTime] | |
| .filter((t): t is string => !!t) | |
| .sort() | |
| .reverse()[0]; | |
| return { | |
| remaining: (proQuota?.remaining ?? 0) + (flashQuota?.remaining ?? 0), | |
| limit: (proQuota?.limit ?? 0) + (flashQuota?.limit ?? 0), | |
| resetTime, | |
| }; | |
| } | |
| return {}; | |
| } | |
| getQuotaRemaining(): number | undefined { | |
| const pooled = this.getPooledQuota(); | |
| if (pooled.remaining !== undefined) { | |
| return pooled.remaining; | |
| } | |
| const primaryModel = resolveModel( | |
| this.getModel(), | |
| this.getGemini31LaunchedSync(), | |
| this.getUseCustomToolModelSync(), | |
| this.getHasAccessToPreviewModel(), | |
| this, | |
| this.hasGemini35FlashGAAccess(), | |
| ); | |
| return this.modelQuotas.get(primaryModel)?.remaining; | |
| } | |
| getQuotaLimit(): number | undefined { | |
| const pooled = this.getPooledQuota(); | |
| if (pooled.limit !== undefined) { | |
| return pooled.limit; | |
| } | |
| const primaryModel = resolveModel( | |
| this.getModel(), | |
| this.getGemini31LaunchedSync(), | |
| this.getUseCustomToolModelSync(), | |
| this.getHasAccessToPreviewModel(), | |
| this, | |
| this.hasGemini35FlashGAAccess(), | |
| ); | |
| return this.modelQuotas.get(primaryModel)?.limit; | |
| } | |
| getQuotaResetTime(): string | undefined { | |
| const pooled = this.getPooledQuota(); | |
| if (pooled.resetTime !== undefined) { | |
| return pooled.resetTime; | |
| } | |
| const primaryModel = resolveModel( | |
| this.getModel(), | |
| this.getGemini31LaunchedSync(), | |
| this.getUseCustomToolModelSync(), | |
| this.getHasAccessToPreviewModel(), | |
| this, | |
| this.hasGemini35FlashGAAccess(), | |
| ); | |
| return this.modelQuotas.get(primaryModel)?.resetTime; | |
| } | |
| getEmbeddingModel(): string { | |
| return this.embeddingModel; | |
| } | |
| getSandbox(): SandboxConfig | undefined { | |
| return this.sandbox; | |
| } | |
| getSandboxEnabled(): boolean { | |
| return this.sandbox?.enabled ?? false; | |
| } | |
| getSandboxAllowedPaths(): string[] { | |
| const paths = [...(this.sandbox?.allowedPaths ?? [])]; | |
| const globalTempDir = Storage.getGlobalTempDir(); | |
| if (!paths.includes(globalTempDir)) { | |
| paths.push(globalTempDir); | |
| } | |
| return paths; | |
| } | |
| getSandboxNetworkAccess(): boolean { | |
| return this.sandbox?.networkAccess ?? false; | |
| } | |
| isRestrictiveSandbox(): boolean { | |
| const sandboxConfig = this.getSandbox(); | |
| const seatbeltProfile = process.env['SEATBELT_PROFILE']; | |
| return ( | |
| !!sandboxConfig && | |
| sandboxConfig.command === 'sandbox-exec' && | |
| !!seatbeltProfile && | |
| (seatbeltProfile.startsWith('restrictive-') || | |
| seatbeltProfile.startsWith('strict-')) | |
| ); | |
| } | |
| getTargetDir(): string { | |
| return this.targetDir; | |
| } | |
| getProjectRoot(): string { | |
| return this.targetDir; | |
| } | |
| /** | |
| * Returns the path to the ripgrep binary, or null if not found or unsafe. | |
| * Uses Promise-based caching to prevent race conditions and redundant I/O. | |
| */ | |
| async getRipgrepPath(): Promise<string | null> { | |
| if (!this._ripgrepPathPromise) { | |
| this._ripgrepPathPromise = resolveRipgrepPath(); | |
| } | |
| return this._ripgrepPathPromise; | |
| } | |
| /** | |
| * Checks if ripgrep is available. | |
| */ | |
| async canUseRipgrep(): Promise<boolean> { | |
| return (await this.getRipgrepPath()) !== null; | |
| } | |
| /** | |
| * Resets the cached ripgrep path. Used for testing. | |
| * @internal | |
| */ | |
| __resetRipgrepPathCache(): void { | |
| this._ripgrepPathPromise = undefined; | |
| } | |
| getWorkspaceContext(): WorkspaceContext { | |
| return getWorkspaceContextOverride() ?? this.workspaceContext; | |
| } | |
| private refreshSessionScopedPlansDirectory(previousPlansDir: string): void { | |
| const nextPlansDir = this.storage.getPlansDir(); | |
| if (previousPlansDir === nextPlansDir) { | |
| return; | |
| } | |
| const pathsToRemove = new Set([previousPlansDir]); | |
| try { | |
| pathsToRemove.add(resolveToRealPath(previousPlansDir)); | |
| } catch { | |
| // The previous session's plans directory may never have been created. | |
| // In that case there is nothing to resolve or remove beyond the raw path. | |
| } | |
| const currentDirectories = this.workspaceContext | |
| .getDirectories() | |
| .filter((dir) => !pathsToRemove.has(dir)); | |
| this.workspaceContext.setDirectories(currentDirectories); | |
| try { | |
| if (fs.existsSync(nextPlansDir)) { | |
| this.workspaceContext.addDirectory(nextPlansDir); | |
| } | |
| } catch { | |
| // Ignore invalid or unreadable plans directories here. This mirrors | |
| // initialization behavior, which only adds the plans directory when it | |
| // already exists and is readable. | |
| } | |
| } | |
| getAgentRegistry(): AgentRegistry { | |
| return this.agentRegistry; | |
| } | |
| getAcknowledgedAgentsService(): AcknowledgedAgentsService { | |
| return this.acknowledgedAgentsService; | |
| } | |
| /** @deprecated Use toolRegistry getter */ | |
| getToolRegistry(): ToolRegistry { | |
| return this.toolRegistry; | |
| } | |
| getPromptRegistry(): PromptRegistry { | |
| return this.promptRegistry; | |
| } | |
| getSkillManager(): SkillManager { | |
| return this.skillManager; | |
| } | |
| getResourceRegistry(): ResourceRegistry { | |
| return this.resourceRegistry; | |
| } | |
| getDebugMode(): boolean { | |
| return this.debugMode; | |
| } | |
| getQuestion(): string | undefined { | |
| return this.question; | |
| } | |
| getHasAccessToPreviewModel(): boolean { | |
| return this.hasAccessToPreviewModel ?? false; | |
| } | |
| setHasAccessToPreviewModel(hasAccess: boolean | null): void { | |
| this.hasAccessToPreviewModel = hasAccess; | |
| } | |
| async refreshAvailableCredits(): Promise<void> { | |
| const codeAssistServer = getCodeAssistServer(this); | |
| if (!codeAssistServer) { | |
| return; | |
| } | |
| try { | |
| await codeAssistServer.refreshAvailableCredits(); | |
| } catch { | |
| // Non-fatal: proceed even if refresh fails. | |
| // The actual credit balance will be verified server-side. | |
| } | |
| } | |
| async refreshUserQuota(): Promise<RetrieveUserQuotaResponse | undefined> { | |
| const codeAssistServer = getCodeAssistServer(this); | |
| if (!codeAssistServer || !codeAssistServer.projectId) { | |
| return undefined; | |
| } | |
| try { | |
| const quota = await codeAssistServer.retrieveUserQuota({ | |
| project: codeAssistServer.projectId, | |
| }); | |
| if (quota.buckets) { | |
| this.lastRetrievedQuota = quota; | |
| this.lastQuotaFetchTime = Date.now(); | |
| for (const bucket of quota.buckets) { | |
| if (!bucket.modelId || bucket.remainingFraction == null) { | |
| continue; | |
| } | |
| let modelId = bucket.modelId; | |
| if (modelId === SECONDARY_GEMINI_3_5_FLASH_MODEL) { | |
| modelId = DEFAULT_GEMINI_3_5_FLASH_MODEL; | |
| } | |
| let remaining: number; | |
| let limit: number; | |
| if (bucket.remainingAmount) { | |
| remaining = parseInt(bucket.remainingAmount, 10); | |
| limit = | |
| bucket.remainingFraction > 0 | |
| ? Math.round(remaining / bucket.remainingFraction) | |
| : (this.modelQuotas.get(modelId)?.limit ?? 0); | |
| } else { | |
| // Server only sent remainingFraction — use a normalized scale. | |
| limit = 100; | |
| remaining = Math.round(bucket.remainingFraction * limit); | |
| } | |
| if (!isNaN(remaining) && Number.isFinite(limit) && limit > 0) { | |
| this.modelQuotas.set(modelId, { | |
| remaining, | |
| limit, | |
| resetTime: bucket.resetTime, | |
| }); | |
| } | |
| } | |
| } | |
| const hasAccess = | |
| quota.buckets?.some( | |
| (b) => b.modelId && isPreviewModel(b.modelId, this), | |
| ) ?? false; | |
| this.setHasAccessToPreviewModel(hasAccess); | |
| if (quota.buckets) { | |
| this.emitQuotaChangedEvent(); | |
| } | |
| return quota; | |
| } catch (e) { | |
| debugLogger.debug('Failed to retrieve user quota', e); | |
| return undefined; | |
| } | |
| } | |
| async refreshUserQuotaIfStale( | |
| staleMs = 30_000, | |
| ): Promise<RetrieveUserQuotaResponse | undefined> { | |
| const now = Date.now(); | |
| if (now - this.lastQuotaFetchTime > staleMs) { | |
| return this.refreshUserQuota(); | |
| } | |
| return this.lastRetrievedQuota; | |
| } | |
| getLastRetrievedQuota(): RetrieveUserQuotaResponse | undefined { | |
| return this.lastRetrievedQuota; | |
| } | |
| getRemainingQuotaForModel(modelId: string): | |
| | { | |
| remainingAmount?: number; | |
| remainingFraction?: number; | |
| resetTime?: string; | |
| } | |
| | undefined { | |
| const bucket = this.lastRetrievedQuota?.buckets?.find( | |
| (b) => b.modelId === modelId, | |
| ); | |
| if (!bucket) return undefined; | |
| return { | |
| remainingAmount: bucket.remainingAmount | |
| ? parseInt(bucket.remainingAmount, 10) | |
| : undefined, | |
| remainingFraction: bucket.remainingFraction, | |
| resetTime: bucket.resetTime, | |
| }; | |
| } | |
| getCoreTools(): string[] | undefined { | |
| return this.coreTools; | |
| } | |
| getMainAgentTools(): string[] | undefined { | |
| return this.mainAgentTools; | |
| } | |
| getAllowedTools(): string[] | undefined { | |
| return this.allowedTools; | |
| } | |
| /** | |
| * All the excluded tools from static configuration, loaded extensions, or | |
| * other sources (like the Policy Engine). | |
| * | |
| * May change over time. | |
| */ | |
| getExcludeTools( | |
| toolMetadata?: Map<string, Record<string, unknown>>, | |
| allToolNames?: Set<string>, | |
| ): Set<string> | undefined { | |
| // Right now this is present for backward compatibility with settings.json exclude | |
| const excludeToolsSet = new Set([...(this.excludeTools ?? [])]); | |
| for (const extension of this.getExtensionLoader().getExtensions()) { | |
| if (!extension.isActive) { | |
| continue; | |
| } | |
| for (const tool of extension.excludeTools || []) { | |
| excludeToolsSet.add(tool); | |
| } | |
| } | |
| const policyExclusions = this.policyEngine.getExcludedTools( | |
| toolMetadata, | |
| allToolNames, | |
| ); | |
| for (const tool of policyExclusions) { | |
| excludeToolsSet.add(tool); | |
| } | |
| return excludeToolsSet; | |
| } | |
| getToolDiscoveryCommand(): string | undefined { | |
| return this.toolDiscoveryCommand; | |
| } | |
| getToolCallCommand(): string | undefined { | |
| return this.toolCallCommand; | |
| } | |
| getMcpServerCommand(): string | undefined { | |
| return this.mcpServerCommand; | |
| } | |
| /** | |
| * The user configured MCP servers (via gemini settings files). | |
| * | |
| * Does NOT include mcp servers configured by extensions. | |
| */ | |
| getMcpServers(): Record<string, MCPServerConfig> | undefined { | |
| return this.mcpServers; | |
| } | |
| getMcpEnabled(): boolean { | |
| return this.mcpEnabled; | |
| } | |
| getMcpEnablementCallbacks(): McpEnablementCallbacks | undefined { | |
| return this.mcpEnablementCallbacks; | |
| } | |
| getExtensionsEnabled(): boolean { | |
| return this.extensionsEnabled; | |
| } | |
| getExtensionRegistryURI(): string | undefined { | |
| return this.extensionRegistryURI; | |
| } | |
| getMcpClientManager(): McpClientManager | undefined { | |
| return this.mcpClientManager; | |
| } | |
| getA2AClientManager(): A2AClientManager | undefined { | |
| return this.a2aClientManager; | |
| } | |
| setUserInteractedWithMcp(): void { | |
| this.mcpClientManager?.setUserInteractedWithMcp(); | |
| } | |
| /** @deprecated Use getMcpClientManager().getLastError() directly */ | |
| getLastMcpError(serverName: string): string | undefined { | |
| return this.mcpClientManager?.getLastError(serverName); | |
| } | |
| emitMcpDiagnostic( | |
| severity: 'info' | 'warning' | 'error', | |
| message: string, | |
| error?: unknown, | |
| serverName?: string, | |
| ): void { | |
| if (this.mcpClientManager) { | |
| this.mcpClientManager.emitDiagnostic( | |
| severity, | |
| message, | |
| error, | |
| serverName, | |
| ); | |
| } else { | |
| coreEvents.emitFeedback(severity, message, error); | |
| } | |
| } | |
| getAllowedMcpServers(): string[] | undefined { | |
| return this.allowedMcpServers; | |
| } | |
| getBlockedMcpServers(): string[] | undefined { | |
| return this.blockedMcpServers; | |
| } | |
| get sanitizationConfig(): EnvironmentSanitizationConfig { | |
| return { | |
| allowedEnvironmentVariables: this.allowedEnvironmentVariables, | |
| blockedEnvironmentVariables: this.blockedEnvironmentVariables, | |
| enableEnvironmentVariableRedaction: | |
| this.enableEnvironmentVariableRedaction, | |
| }; | |
| } | |
| setMcpServers(mcpServers: Record<string, MCPServerConfig>): void { | |
| this.mcpServers = mcpServers; | |
| } | |
| getUserMemory(): string | HierarchicalMemory { | |
| if (this.memoryContextManager) { | |
| return { | |
| global: this.memoryContextManager.getGlobalMemory(), | |
| extension: this.memoryContextManager.getExtensionMemory(), | |
| project: this.memoryContextManager.getEnvironmentMemory(), | |
| userProjectMemory: this.memoryContextManager.getUserProjectMemory(), | |
| }; | |
| } | |
| return this.userMemory; | |
| } | |
| /** | |
| * Refreshes the MCP context, including memory, tools, and system instructions. | |
| */ | |
| async refreshMcpContext(): Promise<void> { | |
| await this.memoryContextManager?.refresh(); | |
| if (this.geminiClient?.isInitialized()) { | |
| await this.geminiClient.setTools(); | |
| this.geminiClient.updateSystemInstruction(); | |
| } | |
| } | |
| setUserMemory(newUserMemory: string | HierarchicalMemory): void { | |
| this.userMemory = newUserMemory; | |
| } | |
| /** | |
| * Returns Tier 1 memory for the system instruction. Global memory and user | |
| * project memory go in the system instruction; extension and project memory | |
| * are placed in the first user message instead, per the tiered context model. | |
| * User project memory is in Tier 1 so mid-session saves are reflected via | |
| * system instruction updates. | |
| */ | |
| getSystemInstructionMemory(): string | HierarchicalMemory { | |
| if (this.memoryContextManager) { | |
| const global = this.memoryContextManager.getGlobalMemory(); | |
| const userProjectMemory = | |
| this.memoryContextManager.getUserProjectMemory(); | |
| if (userProjectMemory?.trim()) { | |
| return { global, userProjectMemory }; | |
| } | |
| return global; | |
| } | |
| return this.userMemory; | |
| } | |
| /** | |
| * Returns Tier 2 memory (extension + project) for injection into the first | |
| * user message. | |
| */ | |
| getSessionMemory(options?: { includeExtensionContext?: boolean }): string { | |
| if (!this.memoryContextManager) { | |
| return ''; | |
| } | |
| const sections: string[] = []; | |
| const includeExtensionContext = options?.includeExtensionContext ?? true; | |
| const extension = includeExtensionContext | |
| ? this.memoryContextManager.getExtensionMemory() | |
| : ''; | |
| const project = this.memoryContextManager.getEnvironmentMemory(); | |
| if (extension?.trim()) { | |
| sections.push( | |
| `<extension_context>\n${extension.trim()}\n</extension_context>`, | |
| ); | |
| } | |
| if (project?.trim()) { | |
| sections.push(`<project_context>\n${project.trim()}\n</project_context>`); | |
| } | |
| if (sections.length === 0) return ''; | |
| return `\n<loaded_context>\n${sections.join('\n')}\n</loaded_context>`; | |
| } | |
| getGlobalMemory(): string { | |
| return this.memoryContextManager?.getGlobalMemory() ?? ''; | |
| } | |
| getEnvironmentMemory(): string { | |
| return this.memoryContextManager?.getEnvironmentMemory() ?? ''; | |
| } | |
| getMemoryContextManager(): MemoryContextManager | undefined { | |
| return this.memoryContextManager; | |
| } | |
| isContextManagementEnabled(): boolean { | |
| return this.contextManagement.enabled; | |
| } | |
| isAgentSessionSubagentEnabled(): boolean { | |
| return this.agentSessionSubagentEnabled; | |
| } | |
| getMemoryBoundaryMarkers(): readonly string[] { | |
| return this.memoryBoundaryMarkers; | |
| } | |
| isAutoMemoryEnabled(): boolean { | |
| return this.experimentalAutoMemory; | |
| } | |
| getExperimentalGemma(): boolean { | |
| return this.experimentalGemma; | |
| } | |
| getExperimentalContextManagementConfig(): string | undefined { | |
| return this.experimentalContextManagementConfig; | |
| } | |
| getContextManagementConfig(): ContextManagementConfig { | |
| return this.contextManagement; | |
| } | |
| get agentHistoryProviderConfig(): AgentHistoryProviderConfig { | |
| return { | |
| maxTokens: this.contextManagement.historyWindow.maxTokens, | |
| retainedTokens: this.contextManagement.historyWindow.retainedTokens, | |
| normalMessageTokens: this.contextManagement.messageLimits.normalMaxTokens, | |
| maximumMessageTokens: | |
| this.contextManagement.messageLimits.retainedMaxTokens, | |
| normalizationHeadRatio: | |
| this.contextManagement.messageLimits.normalizationHeadRatio, | |
| }; | |
| } | |
| isTopicUpdateNarrationEnabled(): boolean { | |
| return this.topicUpdateNarration; | |
| } | |
| isModelSteeringEnabled(): boolean { | |
| return this.modelSteering; | |
| } | |
| async getToolOutputMaskingConfig(): Promise<ToolOutputMaskingConfig> { | |
| await this.ensureExperimentsLoaded(); | |
| const remoteProtection = | |
| this.experiments?.flags[ExperimentFlags.MASKING_PROTECTION_THRESHOLD] | |
| ?.intValue; | |
| const remotePrunable = | |
| this.experiments?.flags[ExperimentFlags.MASKING_PRUNABLE_THRESHOLD] | |
| ?.intValue; | |
| const remoteProtectLatest = | |
| this.experiments?.flags[ExperimentFlags.MASKING_PROTECT_LATEST_TURN] | |
| ?.boolValue; | |
| const parsedProtection = remoteProtection | |
| ? parseInt(remoteProtection, 10) | |
| : undefined; | |
| const parsedPrunable = remotePrunable | |
| ? parseInt(remotePrunable, 10) | |
| : undefined; | |
| return { | |
| protectionThresholdTokens: | |
| parsedProtection !== undefined && !isNaN(parsedProtection) | |
| ? parsedProtection | |
| : this.contextManagement.tools.outputMasking | |
| .protectionThresholdTokens, | |
| minPrunableThresholdTokens: | |
| parsedPrunable !== undefined && !isNaN(parsedPrunable) | |
| ? parsedPrunable | |
| : this.contextManagement.tools.outputMasking | |
| .minPrunableThresholdTokens, | |
| protectLatestTurn: | |
| remoteProtectLatest ?? | |
| this.contextManagement.tools.outputMasking.protectLatestTurn, | |
| }; | |
| } | |
| getGeminiMdFileCount(): number { | |
| if (this.memoryContextManager) { | |
| return this.memoryContextManager.getLoadedPaths().size; | |
| } | |
| return this.geminiMdFileCount; | |
| } | |
| setGeminiMdFileCount(count: number): void { | |
| this.geminiMdFileCount = count; | |
| } | |
| getGeminiMdFilePaths(): string[] { | |
| if (this.memoryContextManager) { | |
| return Array.from(this.memoryContextManager.getLoadedPaths()); | |
| } | |
| return this.geminiMdFilePaths; | |
| } | |
| getWorkspacePoliciesDir(): string | undefined { | |
| return this.workspacePoliciesDir; | |
| } | |
| setGeminiMdFilePaths(paths: string[]): void { | |
| this.geminiMdFilePaths = paths; | |
| } | |
| getApprovalMode(): ApprovalMode { | |
| return this.policyEngine.getApprovalMode(); | |
| } | |
| isPlanMode(): boolean { | |
| return this.getApprovalMode() === ApprovalMode.PLAN; | |
| } | |
| getPolicyUpdateConfirmationRequest(): | |
| | PolicyUpdateConfirmationRequest | |
| | undefined { | |
| return this.policyUpdateConfirmationRequest; | |
| } | |
| /** | |
| * Hot-loads workspace policies from the specified directory into the active policy engine. | |
| * This allows applying newly accepted policies without requiring an application restart. | |
| * | |
| * @param policyDir The directory containing the workspace policy TOML files. | |
| */ | |
| async loadWorkspacePolicies(policyDir: string): Promise<void> { | |
| const { rules, checkers } = await loadPoliciesFromToml( | |
| [policyDir], | |
| () => WORKSPACE_POLICY_TIER, | |
| ); | |
| // Clear existing workspace policies to prevent duplicates/stale rules | |
| this.policyEngine.removeRulesByTier(WORKSPACE_POLICY_TIER); | |
| this.policyEngine.removeCheckersByTier(WORKSPACE_POLICY_TIER); | |
| for (const rule of rules) { | |
| this.policyEngine.addRule(rule); | |
| } | |
| for (const checker of checkers) { | |
| this.policyEngine.addChecker(checker); | |
| } | |
| this.policyUpdateConfirmationRequest = undefined; | |
| debugLogger.debug(`Workspace policies loaded from: ${policyDir}`); | |
| } | |
| setApprovalMode(mode: ApprovalMode): void { | |
| if ( | |
| !this.isTrustedFolder() && | |
| mode !== ApprovalMode.DEFAULT && | |
| mode !== ApprovalMode.PLAN | |
| ) { | |
| throw new Error( | |
| 'Cannot enable privileged approval modes in an untrusted folder.', | |
| ); | |
| } | |
| const currentMode = this.getApprovalMode(); | |
| if (currentMode !== mode) { | |
| this.logCurrentModeDuration(currentMode); | |
| logApprovalModeSwitch( | |
| this, | |
| new ApprovalModeSwitchEvent(currentMode, mode), | |
| ); | |
| this.policyEngine.setApprovalMode(mode); | |
| this.refreshSandboxManager(); | |
| coreEvents.emit(CoreEvent.ApprovalModeChanged, { | |
| sessionId: this.getSessionId(), | |
| mode, | |
| }); | |
| const isPlanModeTransition = | |
| currentMode === ApprovalMode.PLAN || mode === ApprovalMode.PLAN; | |
| const isYoloModeTransition = | |
| currentMode === ApprovalMode.YOLO || mode === ApprovalMode.YOLO; | |
| if (isPlanModeTransition || isYoloModeTransition) { | |
| if (this.geminiClient?.isInitialized()) { | |
| this.geminiClient.clearCurrentSequenceModel(); | |
| this.geminiClient.setTools().catch((err) => { | |
| debugLogger.error('Failed to update tools', err); | |
| }); | |
| } | |
| this.updateSystemInstructionIfInitialized(); | |
| } | |
| } | |
| } | |
| /** | |
| * Logs the duration of the current approval mode. | |
| */ | |
| logCurrentModeDuration(mode: ApprovalMode): void { | |
| const now = performance.now(); | |
| const duration = now - this.lastModeSwitchTime; | |
| if (duration > 0) { | |
| logApprovalModeDuration( | |
| this, | |
| new ApprovalModeDurationEvent(mode, duration), | |
| ); | |
| } | |
| this.lastModeSwitchTime = now; | |
| } | |
| isYoloModeDisabled(): boolean { | |
| return this.disableYoloMode || !this.isTrustedFolder(); | |
| } | |
| getDisableAlwaysAllow(): boolean { | |
| return this.disableAlwaysAllow; | |
| } | |
| getRawOutput(): boolean { | |
| return this.rawOutput; | |
| } | |
| getAcceptRawOutputRisk(): boolean { | |
| return this.acceptRawOutputRisk; | |
| } | |
| getExperimentalDynamicModelConfiguration(): boolean { | |
| return this.dynamicModelConfiguration; | |
| } | |
| getReleaseChannel(): string { | |
| return getChannelFromVersion(this._clientVersion); | |
| } | |
| getPendingIncludeDirectories(): string[] { | |
| return this.pendingIncludeDirectories; | |
| } | |
| clearPendingIncludeDirectories(): void { | |
| this.pendingIncludeDirectories = []; | |
| } | |
| getShowMemoryUsage(): boolean { | |
| return this.showMemoryUsage; | |
| } | |
| getAccessibility(): AccessibilitySettings { | |
| return this.accessibility; | |
| } | |
| getLogRagSnippets(): boolean { | |
| return this.logRagSnippets; | |
| } | |
| getTelemetryEnabled(): boolean { | |
| return this.telemetrySettings.enabled ?? false; | |
| } | |
| getTelemetryTracesEnabled(): boolean { | |
| return this.telemetrySettings.traces ?? false; | |
| } | |
| getTelemetryLogPromptsEnabled(): boolean { | |
| return this.telemetrySettings.logPrompts ?? true; | |
| } | |
| getTelemetryOtlpEndpoint(): string { | |
| return this.telemetrySettings.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT; | |
| } | |
| getTelemetryOtlpProtocol(): 'grpc' | 'http' { | |
| return this.telemetrySettings.otlpProtocol ?? 'grpc'; | |
| } | |
| getTelemetryTarget(): TelemetryTarget { | |
| return this.telemetrySettings.target ?? DEFAULT_TELEMETRY_TARGET; | |
| } | |
| getTelemetryOutfile(): string | undefined { | |
| return this.telemetrySettings.outfile; | |
| } | |
| getBillingSettings(): { overageStrategy: OverageStrategy } { | |
| return this.billing; | |
| } | |
| /** | |
| * Updates the overage strategy at runtime. | |
| * Used to switch from 'ask' to 'always' after the user accepts credits | |
| * via the overage dialog, so subsequent API calls auto-include credits. | |
| */ | |
| setOverageStrategy(strategy: OverageStrategy): void { | |
| this.billing.overageStrategy = strategy; | |
| } | |
| getTelemetryUseCollector(): boolean { | |
| return this.telemetrySettings.useCollector ?? false; | |
| } | |
| getTelemetryUseCliAuth(): boolean { | |
| return this.telemetrySettings.useCliAuth ?? false; | |
| } | |
| /** @deprecated Use geminiClient getter */ | |
| getGeminiClient(): GeminiClient { | |
| return this.geminiClient; | |
| } | |
| /** | |
| * Updates the system instruction with the latest user memory. | |
| * Whenever the user memory (GEMINI.md files) is updated. | |
| */ | |
| updateSystemInstructionIfInitialized(): void { | |
| const geminiClient = this.geminiClient; | |
| if (geminiClient?.isInitialized()) { | |
| geminiClient.updateSystemInstruction(); | |
| } | |
| } | |
| getModelRouterService(): ModelRouterService { | |
| return this.modelRouterService; | |
| } | |
| getModelConfigService(): ModelConfigService { | |
| return this.modelConfigService; | |
| } | |
| getModelAvailabilityService(): ModelAvailabilityService { | |
| return this.modelAvailabilityService; | |
| } | |
| getEnableRecursiveFileSearch(): boolean { | |
| return this.fileFiltering.enableRecursiveFileSearch; | |
| } | |
| getFileFilteringEnableFuzzySearch(): boolean { | |
| return this.fileFiltering.enableFuzzySearch; | |
| } | |
| getFileFilteringRespectGitIgnore(): boolean { | |
| return this.fileFiltering.respectGitIgnore; | |
| } | |
| getFileFilteringRespectGeminiIgnore(): boolean { | |
| return this.fileFiltering.respectGeminiIgnore; | |
| } | |
| getCustomIgnoreFilePaths(): string[] { | |
| return this.fileFiltering.customIgnoreFilePaths; | |
| } | |
| getFileFilteringOptions(): FileFilteringOptions { | |
| return { | |
| respectGitIgnore: this.fileFiltering.respectGitIgnore, | |
| respectGeminiIgnore: this.fileFiltering.respectGeminiIgnore, | |
| enableFileWatcher: this.fileFiltering.enableFileWatcher, | |
| maxFileCount: this.fileFiltering.maxFileCount, | |
| searchTimeout: this.fileFiltering.searchTimeout, | |
| customIgnoreFilePaths: this.fileFiltering.customIgnoreFilePaths, | |
| }; | |
| } | |
| /** | |
| * Gets custom file exclusion patterns from configuration. | |
| * TODO: This is a placeholder implementation. In the future, this could | |
| * read from settings files, CLI arguments, or environment variables. | |
| */ | |
| getCustomExcludes(): string[] { | |
| // Placeholder implementation - returns empty array for now | |
| // Future implementation could read from: | |
| // - User settings file | |
| // - Project-specific configuration | |
| // - Environment variables | |
| // - CLI arguments | |
| return []; | |
| } | |
| getCheckpointingEnabled(): boolean { | |
| return this.checkpointing; | |
| } | |
| getProxy(): string | undefined { | |
| return this.proxy; | |
| } | |
| getWorkingDir(): string { | |
| return this.cwd; | |
| } | |
| getBugCommand(): BugCommandSettings | undefined { | |
| return this.bugCommand; | |
| } | |
| getTrackerService(): TrackerService { | |
| if (!this.trackerService) { | |
| this.trackerService = new TrackerService( | |
| this.storage.getProjectTempTrackerDir(), | |
| ); | |
| } | |
| return this.trackerService; | |
| } | |
| getFileService(): FileDiscoveryService { | |
| if (!this.fileDiscoveryService) { | |
| this.fileDiscoveryService = new FileDiscoveryService(this.targetDir, { | |
| respectGitIgnore: this.fileFiltering.respectGitIgnore, | |
| respectGeminiIgnore: this.fileFiltering.respectGeminiIgnore, | |
| customIgnoreFilePaths: this.fileFiltering.customIgnoreFilePaths, | |
| }); | |
| } | |
| return this.fileDiscoveryService; | |
| } | |
| getUsageStatisticsEnabled(): boolean { | |
| return this.usageStatisticsEnabled; | |
| } | |
| getAcpMode(): boolean { | |
| return this.acpMode; | |
| } | |
| async waitForMcpInit(): Promise<void> { | |
| if (this.mcpInitializationPromise) { | |
| await this.mcpInitializationPromise; | |
| } | |
| } | |
| getListExtensions(): boolean { | |
| return this.listExtensions; | |
| } | |
| getListSessions(): boolean { | |
| return this.listSessions; | |
| } | |
| getDeleteSession(): string | undefined { | |
| return this.deleteSession; | |
| } | |
| getExtensionManagement(): boolean { | |
| return this.extensionManagement; | |
| } | |
| getExtensions(): GeminiCLIExtension[] { | |
| return this._extensionLoader.getExtensions(); | |
| } | |
| getExtensionLoader(): ExtensionLoader { | |
| return this._extensionLoader; | |
| } | |
| // The list of explicitly enabled extensions, if any were given, may contain | |
| // the string "none". | |
| getEnabledExtensions(): string[] { | |
| return this._enabledExtensions; | |
| } | |
| getEnableExtensionReloading(): boolean { | |
| return this.enableExtensionReloading; | |
| } | |
| getDisableLLMCorrection(): boolean { | |
| return this.disableLLMCorrection; | |
| } | |
| isPlanEnabled(): boolean { | |
| return this.planEnabled; | |
| } | |
| isVoiceModeEnabled(): boolean { | |
| return this.voiceMode; | |
| } | |
| isTrackerEnabled(): boolean { | |
| return this.trackerEnabled; | |
| } | |
| getApprovedPlanPath(): string | undefined { | |
| return this.approvedPlanPath; | |
| } | |
| getDirectWebFetch(): boolean { | |
| return this.directWebFetch; | |
| } | |
| setApprovedPlanPath(path: string | undefined): void { | |
| this.approvedPlanPath = path; | |
| } | |
| isAgentsEnabled(): boolean { | |
| return this.enableAgents; | |
| } | |
| isEventDrivenSchedulerEnabled(): boolean { | |
| return this.enableEventDrivenScheduler; | |
| } | |
| getNoBrowser(): boolean { | |
| return this.noBrowser; | |
| } | |
| getAgentsSettings(): AgentSettings { | |
| return this.agents; | |
| } | |
| isBrowserLaunchSuppressed(): boolean { | |
| return this.getNoBrowser() || !shouldAttemptBrowserLaunch(); | |
| } | |
| getSummarizeToolOutputConfig(): | |
| | Record<string, SummarizeToolOutputSettings> | |
| | undefined { | |
| return this.summarizeToolOutput; | |
| } | |
| getIdeMode(): boolean { | |
| return this.ideMode; | |
| } | |
| /** | |
| * Returns 'true' if the folder trust feature is enabled. | |
| */ | |
| getFolderTrust(): boolean { | |
| return this.folderTrust; | |
| } | |
| /** | |
| * Returns 'true' if the workspace is considered "trusted". | |
| * 'false' for untrusted. | |
| */ | |
| isTrustedFolder(): boolean { | |
| const { isTrusted, source } = checkPathTrust({ | |
| path: this.targetDir, | |
| isFolderTrustEnabled: this.folderTrust, | |
| }); | |
| if (isTrusted === false) { | |
| return false; | |
| } | |
| if (source === 'env') { | |
| return isTrusted; | |
| } | |
| if (this.trustedFolder !== undefined) { | |
| return this.trustedFolder; | |
| } | |
| return isTrusted ?? false; | |
| } | |
| setIdeMode(value: boolean): void { | |
| this.ideMode = value; | |
| } | |
| private isScopedMemoryInboxPatchPathAllowed( | |
| absolutePath: string, | |
| resolvedPath: string, | |
| inboxRoot: string, | |
| checkType: 'read' | 'write' = 'write', | |
| ): boolean { | |
| if (!hasScopedMemoryInboxAccess()) { | |
| return false; | |
| } | |
| const normalizedPath = path.resolve(absolutePath); | |
| const resolvedMemoryRoot = resolveToRealPath( | |
| this.storage.getProjectMemoryTempDir(), | |
| ); | |
| // Reads: allow the inbox root and the per-kind subtrees so the extraction | |
| // agent can list/inspect prior patches (including non-canonical filenames | |
| // left over from older runs) before deciding how to rewrite the canonical | |
| // extraction.patch. Writes still flow through the strict canonical-path | |
| // check below so the inbox cannot be backdoored with arbitrary files. | |
| if (checkType === 'read') { | |
| const resolvedInboxRoot = resolveToRealPath(inboxRoot); | |
| const normalizedInboxRoot = path.resolve(inboxRoot); | |
| if ( | |
| resolvedPath === resolvedInboxRoot || | |
| normalizedPath === normalizedInboxRoot | |
| ) { | |
| return isSubpath(resolvedMemoryRoot, resolvedPath); | |
| } | |
| for (const kind of ['private', 'global'] as const) { | |
| const kindRoot = path.join(inboxRoot, kind); | |
| const resolvedKindRoot = resolveToRealPath(kindRoot); | |
| const normalizedKindRoot = path.resolve(kindRoot); | |
| if ( | |
| resolvedPath === resolvedKindRoot || | |
| normalizedPath === normalizedKindRoot || | |
| isSubpath(resolvedKindRoot, resolvedPath) || | |
| isSubpath(normalizedKindRoot, normalizedPath) | |
| ) { | |
| return isSubpath(resolvedMemoryRoot, resolvedPath); | |
| } | |
| } | |
| return false; | |
| } | |
| const isCanonicalPatchPath = (['private', 'global'] as const).some( | |
| (kind) => | |
| normalizedPath === path.resolve(inboxRoot, kind, 'extraction.patch'), | |
| ); | |
| if (!isCanonicalPatchPath) { | |
| return false; | |
| } | |
| return isSubpath(resolvedMemoryRoot, resolvedPath); | |
| } | |
| private isScopedAutoMemoryExtractionWritePathAllowed( | |
| absolutePath: string, | |
| resolvedPath: string, | |
| ): boolean { | |
| if (!hasScopedAutoMemoryExtractionWriteAccess()) { | |
| return false; | |
| } | |
| const resolvedSkillsMemoryDir = resolveToRealPath( | |
| this.storage.getProjectSkillsMemoryDir(), | |
| ); | |
| if (isSubpath(resolvedSkillsMemoryDir, resolvedPath)) { | |
| return true; | |
| } | |
| return this.isScopedMemoryInboxPatchPathAllowed( | |
| absolutePath, | |
| resolvedPath, | |
| path.join(this.storage.getProjectMemoryTempDir(), '.inbox'), | |
| ); | |
| } | |
| /** | |
| * Get the current FileSystemService | |
| */ | |
| getFileSystemService(): FileSystemService { | |
| return this.fileSystemService; | |
| } | |
| /** | |
| * Checks if a given absolute path is allowed for file system operations. | |
| * A path is allowed if it's within the workspace context, the project's | |
| * temporary directory, or is exactly the global personal `~/.gemini/GEMINI.md` | |
| * file (the latter is the only file under `~/.gemini/` that is reachable — | |
| * settings, credentials, keybindings, etc. remain disallowed). | |
| * | |
| * One subtree is *carved back out*: `<projectMemoryDir>/.inbox/` is owned by | |
| * the auto-memory extraction agent and the `/memory inbox` review flow. The | |
| * main agent is denied access to it even though it falls inside the project | |
| * temp dir; the extraction agent receives a narrow execution-scoped exception | |
| * for *writes* to `.inbox/{private,global}/extraction.patch`. Scoped *read* | |
| * access to the wider `.inbox/{private,global}/` subtree is granted in | |
| * `validatePathAccess` so the extractor can enumerate prior patches. | |
| * | |
| * @param absolutePath The absolute path to check. | |
| * @returns true if the path is allowed, false otherwise. | |
| */ | |
| isPathAllowed(absolutePath: string): boolean { | |
| const resolvedPath = resolveToRealPath(absolutePath); | |
| // The auto-memory inbox (`<projectMemoryDir>/.inbox/`) is owned by the | |
| // background extraction agent and the `/memory inbox` review flow. The | |
| // main agent must NOT drop files into it directly (that would let the | |
| // model bypass review). Deny first, even if the path also satisfies the | |
| // workspace or project-temp allowlists below. | |
| const inboxRoot = path.join( | |
| this.storage.getProjectMemoryTempDir(), | |
| '.inbox', | |
| ); | |
| const resolvedInboxRoot = resolveToRealPath(inboxRoot); | |
| const normalizedPath = path.resolve(absolutePath); | |
| const normalizedInboxRoot = path.resolve(inboxRoot); | |
| if ( | |
| resolvedPath === resolvedInboxRoot || | |
| isSubpath(resolvedInboxRoot, resolvedPath) || | |
| normalizedPath === normalizedInboxRoot || | |
| isSubpath(normalizedInboxRoot, normalizedPath) | |
| ) { | |
| if ( | |
| this.isScopedMemoryInboxPatchPathAllowed( | |
| absolutePath, | |
| resolvedPath, | |
| inboxRoot, | |
| ) | |
| ) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| const workspaceContext = this.getWorkspaceContext(); | |
| if (workspaceContext.isPathWithinWorkspace(resolvedPath)) { | |
| return true; | |
| } | |
| const projectTempDir = this.storage.getProjectTempDir(); | |
| const resolvedTempDir = resolveToRealPath(projectTempDir); | |
| if (isSubpath(resolvedTempDir, resolvedPath)) { | |
| return true; | |
| } | |
| // Surgical allowlist: the global personal GEMINI.md file (and ONLY that | |
| // file) is reachable so the prompt-driven memory flow can persist | |
| // cross-project personal preferences. This deliberately does NOT | |
| // allowlist the rest of `~/.gemini/`. | |
| const globalMemoryFilePath = path.join( | |
| Storage.getGlobalGeminiDir(), | |
| getCurrentGeminiMdFilename(), | |
| ); | |
| const resolvedGlobalMemoryFilePath = | |
| resolveToRealPath(globalMemoryFilePath); | |
| if (resolvedPath === resolvedGlobalMemoryFilePath) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Validates if a path is allowed and returns a detailed error message if not. | |
| * | |
| * @param absolutePath The absolute path to validate. | |
| * @param checkType The type of access to check ('read' or 'write'). Defaults to 'write' for safety. | |
| * @returns An error message string if the path is disallowed, null otherwise. | |
| */ | |
| validatePathAccess( | |
| absolutePath: string, | |
| checkType: 'read' | 'write' = 'write', | |
| ): string | null { | |
| const pathValidation = validatePath(absolutePath); | |
| if (!pathValidation.isValid) { | |
| return `Invalid path: ${pathValidation.error}`; | |
| } | |
| if (checkType === 'write' && hasScopedAutoMemoryExtractionWriteAccess()) { | |
| const resolvedPath = resolveToRealPath(absolutePath); | |
| if ( | |
| this.isScopedAutoMemoryExtractionWritePathAllowed( | |
| absolutePath, | |
| resolvedPath, | |
| ) | |
| ) { | |
| return null; | |
| } | |
| return `Auto-memory extraction write denied: Attempted path "${absolutePath}" is outside the extraction write allowlist. Extraction may only write extracted skills under ${this.storage.getProjectSkillsMemoryDir()} and canonical inbox patches under ${path.join(this.storage.getProjectMemoryTempDir(), '.inbox', '{private,global}', 'extraction.patch')}.`; | |
| } | |
| // For read operations, check read-only paths first | |
| if (checkType === 'read') { | |
| if (this.getWorkspaceContext().isPathReadable(absolutePath)) { | |
| return null; | |
| } | |
| // The memory inbox is carved out of the standard temp-dir allowlist by | |
| // `isPathAllowed`. The extraction agent is granted a scoped read | |
| // exception so it can enumerate prior patches (including non-canonical | |
| // filenames) before consolidating them into the canonical | |
| // extraction.patch. Writes remain restricted to canonical paths. | |
| if (hasScopedMemoryInboxAccess()) { | |
| const inboxRoot = path.join( | |
| this.storage.getProjectMemoryTempDir(), | |
| '.inbox', | |
| ); | |
| if ( | |
| this.isScopedMemoryInboxPatchPathAllowed( | |
| absolutePath, | |
| resolveToRealPath(absolutePath), | |
| inboxRoot, | |
| 'read', | |
| ) | |
| ) { | |
| return null; | |
| } | |
| } | |
| } | |
| // Then check standard allowed paths (Workspace + Temp) | |
| // This covers 'write' checks and acts as a fallback/temp-dir check for 'read' | |
| if (this.isPathAllowed(absolutePath)) { | |
| return null; | |
| } | |
| const workspaceDirs = this.getWorkspaceContext().getDirectories(); | |
| const projectTempDir = this.storage.getProjectTempDir(); | |
| return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`; | |
| } | |
| /** | |
| * Set a custom FileSystemService | |
| */ | |
| setFileSystemService(fileSystemService: FileSystemService): void { | |
| this.fileSystemService = fileSystemService; | |
| } | |
| async getCompressionThreshold(): Promise<number | undefined> { | |
| if (this.compressionThreshold) { | |
| return this.compressionThreshold; | |
| } | |
| await this.ensureExperimentsLoaded(); | |
| const remoteThreshold = | |
| this.experiments?.flags[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD] | |
| ?.floatValue; | |
| if (remoteThreshold === 0) { | |
| return undefined; | |
| } | |
| return remoteThreshold; | |
| } | |
| async getUserCaching(): Promise<boolean | undefined> { | |
| await this.ensureExperimentsLoaded(); | |
| return this.experiments?.flags[ExperimentFlags.USER_CACHING]?.boolValue; | |
| } | |
| async getPlanModeRoutingEnabled(): Promise<boolean> { | |
| return this.planModeRoutingEnabled; | |
| } | |
| async getNumericalRoutingEnabled(): Promise<boolean> { | |
| await this.ensureExperimentsLoaded(); | |
| const flag = | |
| this.experiments?.flags[ExperimentFlags.ENABLE_NUMERICAL_ROUTING]; | |
| return flag?.boolValue ?? true; | |
| } | |
| /** | |
| * Returns the resolved complexity threshold for routing. | |
| * If a remote threshold is provided and within range (0-100), it is returned. | |
| * Otherwise, the default threshold (90) is returned. | |
| */ | |
| async getResolvedClassifierThreshold(): Promise<number> { | |
| const remoteValue = await this.getClassifierThreshold(); | |
| const defaultValue = 90; | |
| if ( | |
| remoteValue !== undefined && | |
| !isNaN(remoteValue) && | |
| remoteValue >= 0 && | |
| remoteValue <= 100 | |
| ) { | |
| return remoteValue; | |
| } | |
| return defaultValue; | |
| } | |
| async getClassifierThreshold(): Promise<number | undefined> { | |
| await this.ensureExperimentsLoaded(); | |
| const flag = this.experiments?.flags[ExperimentFlags.CLASSIFIER_THRESHOLD]; | |
| if (flag?.intValue !== undefined) { | |
| return parseInt(flag.intValue, 10); | |
| } | |
| return flag?.floatValue; | |
| } | |
| async getBannerTextNoCapacityIssues(): Promise<string> { | |
| await this.ensureExperimentsLoaded(); | |
| return ( | |
| this.experiments?.flags[ExperimentFlags.BANNER_TEXT_NO_CAPACITY_ISSUES] | |
| ?.stringValue ?? '' | |
| ); | |
| } | |
| async getBannerTextCapacityIssues(): Promise<string> { | |
| await this.ensureExperimentsLoaded(); | |
| return ( | |
| this.experiments?.flags[ExperimentFlags.BANNER_TEXT_CAPACITY_ISSUES] | |
| ?.stringValue ?? '' | |
| ); | |
| } | |
| /** | |
| * Returns whether the user has access to Pro models. | |
| * This is determined by the PRO_MODEL_NO_ACCESS experiment flag. | |
| */ | |
| async getProModelNoAccess(): Promise<boolean> { | |
| await this.ensureExperimentsLoaded(); | |
| return this.getProModelNoAccessSync(); | |
| } | |
| /** | |
| * Returns whether the user has access to Pro models synchronously. | |
| * | |
| * Note: This method should only be called after startup, once experiments have been loaded. | |
| */ | |
| getProModelNoAccessSync(): boolean { | |
| if ( | |
| this.contentGeneratorConfig?.authType !== AuthType.LOGIN_WITH_GOOGLE && | |
| this.contentGeneratorConfig?.authType !== AuthType.COMPUTE_ADC | |
| ) { | |
| return false; | |
| } | |
| return ( | |
| this.experiments?.flags[ExperimentFlags.PRO_MODEL_NO_ACCESS]?.boolValue ?? | |
| false | |
| ); | |
| } | |
| /** | |
| * Returns whether Gemini 3.1 Pro has been launched. | |
| * This method is async and ensures that experiments are loaded before returning the result. | |
| */ | |
| async getGemini31Launched(): Promise<boolean> { | |
| await this.ensureExperimentsLoaded(); | |
| return this.getGemini31LaunchedSync(); | |
| } | |
| /** | |
| * Returns whether the custom tool model should be used. | |
| */ | |
| async getUseCustomToolModel(): Promise<boolean> { | |
| const useGemini3_1 = await this.getGemini31Launched(); | |
| const authType = this.contentGeneratorConfig?.authType; | |
| return useGemini3_1 && authType === AuthType.USE_GEMINI; | |
| } | |
| /** | |
| * Returns whether the custom tool model should be used. | |
| * | |
| * Note: This method should only be called after startup, once experiments have been loaded. | |
| */ | |
| getUseCustomToolModelSync(): boolean { | |
| const useGemini3_1 = this.getGemini31LaunchedSync(); | |
| const authType = this.contentGeneratorConfig?.authType; | |
| return useGemini3_1 && authType === AuthType.USE_GEMINI; | |
| } | |
| private isGemini31LaunchedForAuthType(authType?: AuthType): boolean { | |
| return ( | |
| authType === AuthType.USE_GEMINI || | |
| authType === AuthType.USE_VERTEX_AI || | |
| authType === AuthType.GATEWAY | |
| ); | |
| } | |
| /** | |
| * Returns whether Gemini 3.5 Flash GA has been launched. | |
| * | |
| * Note: This method should only be called after startup, once experiments have been loaded. | |
| */ | |
| hasGemini35FlashGAAccess(): boolean { | |
| const authType = this.contentGeneratorConfig?.authType; | |
| const hasAccess = (() => { | |
| if (this.isGemini31LaunchedForAuthType(authType)) { | |
| return true; | |
| } | |
| return ( | |
| this.experiments?.flags[ExperimentFlags.GEMINI_3_5_FLASH_GA_LAUNCHED] | |
| ?.boolValue ?? false | |
| ); | |
| })(); | |
| // Used to set default flash models based on access | |
| // TODO: Remove once the experiment for 3_5 flash rollut can be cleaned up. | |
| if (hasAccess) { | |
| // Gemini API key users should have the ability to manually select the | |
| // old preview flash model. | |
| if (authType === AuthType.USE_GEMINI) { | |
| setFlashModels('gemini-3-flash-preview', 'gemini-3.5-flash'); | |
| } else { | |
| setFlashModels('gemini-3.5-flash', 'gemini-3.5-flash'); | |
| } | |
| } else { | |
| setFlashModels('gemini-3-flash-preview', 'gemini-2.5-flash'); | |
| } | |
| return hasAccess; | |
| } | |
| /** | |
| * Returns whether Gemini 3.1 has been launched. | |
| * | |
| * Note: This method should only be called after startup, once experiments have been loaded. | |
| * If you need to call this during startup or from an async context, use | |
| * getGemini31Launched instead. | |
| */ | |
| getGemini31LaunchedSync(): boolean { | |
| const authType = this.contentGeneratorConfig?.authType; | |
| if (this.isGemini31LaunchedForAuthType(authType)) { | |
| return true; | |
| } | |
| return ( | |
| this.experiments?.flags[ExperimentFlags.GEMINI_3_1_PRO_LAUNCHED] | |
| ?.boolValue ?? false | |
| ); | |
| } | |
| /** | |
| * Returns the configured default request timeout in milliseconds. | |
| */ | |
| getRequestTimeoutMs(): number | undefined { | |
| const flag = | |
| this.experiments?.flags?.[ExperimentFlags.DEFAULT_REQUEST_TIMEOUT]; | |
| if (flag?.intValue !== undefined) { | |
| const seconds = parseInt(flag.intValue, 10); | |
| if (Number.isInteger(seconds) && seconds >= 0) { | |
| return seconds * 1000; // Convert seconds to milliseconds | |
| } | |
| } | |
| return undefined; | |
| } | |
| /** | |
| * Returns the client version. | |
| */ | |
| get clientVersion(): string { | |
| return this._clientVersion; | |
| } | |
| private async ensureExperimentsLoaded(): Promise<void> { | |
| if (!this.experimentsPromise) { | |
| return; | |
| } | |
| try { | |
| await this.experimentsPromise; | |
| } catch (e) { | |
| debugLogger.debug('Failed to fetch experiments', e); | |
| } | |
| } | |
| isInteractiveShellEnabled(): boolean { | |
| return ( | |
| this.interactive && | |
| this.ptyInfo !== 'child_process' && | |
| this.enableInteractiveShell | |
| ); | |
| } | |
| isSkillsSupportEnabled(): boolean { | |
| return this.skillsSupport; | |
| } | |
| /** | |
| * Reloads skills by re-discovering them from extensions and local directories. | |
| */ | |
| async reloadSkills(): Promise<void> { | |
| if (!this.skillsSupport) { | |
| return; | |
| } | |
| if (this.onReload) { | |
| const refreshed = await this.onReload(); | |
| this.disabledSkills = refreshed.disabledSkills ?? []; | |
| this.getSkillManager().setAdminSettings( | |
| refreshed.adminSkillsEnabled ?? this.adminSkillsEnabled, | |
| ); | |
| } | |
| if (this.getSkillManager().isAdminEnabled()) { | |
| await this.getSkillManager().discoverSkills( | |
| this.storage, | |
| this.getExtensions(), | |
| this.isTrustedFolder(), | |
| ); | |
| this.getSkillManager().setDisabledSkills(this.disabledSkills); | |
| // Re-register ActivateSkillTool to update its schema with the newly discovered skills | |
| if (this.getSkillManager().getSkills().length > 0) { | |
| this.toolRegistry.unregisterTool(ActivateSkillTool.Name); | |
| this.toolRegistry.registerTool( | |
| new ActivateSkillTool(this, this.messageBus), | |
| ); | |
| } else { | |
| this.toolRegistry.unregisterTool(ActivateSkillTool.Name); | |
| } | |
| } else { | |
| this.getSkillManager().clearSkills(); | |
| this.toolRegistry.unregisterTool(ActivateSkillTool.Name); | |
| } | |
| // Notify the client that system instructions might need updating | |
| this.updateSystemInstructionIfInitialized(); | |
| } | |
| /** | |
| * Reloads agent settings. | |
| */ | |
| async reloadAgents(): Promise<void> { | |
| if (this.onReload) { | |
| const refreshed = await this.onReload(); | |
| if (refreshed.agents) { | |
| this.agents = refreshed.agents; | |
| } | |
| } | |
| } | |
| isInteractive(): boolean { | |
| return this.interactive; | |
| } | |
| getUseRipgrep(): boolean { | |
| return this.useRipgrep; | |
| } | |
| getUseBackgroundColor(): boolean { | |
| return this.useBackgroundColor; | |
| } | |
| getUseAlternateBuffer(): boolean { | |
| return this.useAlternateBuffer; | |
| } | |
| getUseTerminalBuffer(): boolean { | |
| return this.useTerminalBuffer; | |
| } | |
| getUseRenderProcess(): boolean { | |
| return this.useRenderProcess; | |
| } | |
| getEnableInteractiveShell(): boolean { | |
| return this.enableInteractiveShell; | |
| } | |
| getShellBackgroundCompletionBehavior(): 'inject' | 'notify' | 'silent' { | |
| return this.shellBackgroundCompletionBehavior; | |
| } | |
| getSkipNextSpeakerCheck(): boolean { | |
| return this.skipNextSpeakerCheck; | |
| } | |
| getRetryFetchErrors(): boolean { | |
| return this.retryFetchErrors; | |
| } | |
| getMaxAttempts(): number { | |
| return this.maxAttempts; | |
| } | |
| getEnableShellOutputEfficiency(): boolean { | |
| return this.enableShellOutputEfficiency; | |
| } | |
| getShellToolInactivityTimeout(): number { | |
| return this.shellToolInactivityTimeout; | |
| } | |
| getShellExecutionConfig(): ShellExecutionConfig { | |
| return this.shellExecutionConfig; | |
| } | |
| setShellExecutionConfig(config: Partial<ShellExecutionConfig>): void { | |
| const definedConfig: Partial<ShellExecutionConfig> = {}; | |
| for (const [k, v] of Object.entries(config)) { | |
| // Only merge properties explicitly provided with a concrete value. | |
| // Filtering out `null` and `undefined` ensures existing system defaults | |
| // are preserved when an extension doesn't want to override them. | |
| if (v != null) { | |
| Object.assign(definedConfig, { [k]: v }); | |
| } | |
| } | |
| // Note: This performs a shallow merge. If the incoming config provides a nested | |
| // object (e.g., sandboxConfig), it will completely overwrite the existing | |
| // nested object rather than merging its individual properties. | |
| this.shellExecutionConfig = { | |
| ...this.shellExecutionConfig, | |
| ...definedConfig, | |
| }; | |
| } | |
| getScreenReader(): boolean { | |
| return this.accessibility.screenReader ?? false; | |
| } | |
| getTruncateToolOutputThreshold(): number { | |
| return Math.min( | |
| // Estimate remaining context window in characters (1 token ~= 4 chars). | |
| 4 * | |
| (tokenLimit(this.model) - uiTelemetryService.getLastPromptTokenCount()), | |
| this.truncateToolOutputThreshold, | |
| ); | |
| } | |
| getToolMaxOutputTokens(): number { | |
| return this.contextManagement.tools.distillation.maxOutputTokens; | |
| } | |
| getToolSummarizationThresholdTokens(): number { | |
| return this.contextManagement.tools.distillation | |
| .summarizationThresholdTokens; | |
| } | |
| getNextCompressionTruncationId(): number { | |
| return ++this.compressionTruncationCounter; | |
| } | |
| getUseWriteTodos(): boolean { | |
| return this.useWriteTodos; | |
| } | |
| getOutputFormat(): OutputFormat { | |
| return this.outputSettings?.format | |
| ? this.outputSettings.format | |
| : OutputFormat.TEXT; | |
| } | |
| async getGitService(): Promise<GitService> { | |
| if (!this.gitService) { | |
| this.gitService = new GitService(this.targetDir, this.storage); | |
| await this.gitService.initialize(); | |
| } | |
| return this.gitService; | |
| } | |
| getFileExclusions(): FileExclusions { | |
| return this.fileExclusions; | |
| } | |
| /** @deprecated Use messageBus getter */ | |
| getMessageBus(): MessageBus { | |
| return this.messageBus; | |
| } | |
| getPolicyEngine(): PolicyEngine { | |
| return this.policyEngine; | |
| } | |
| getEnableHooks(): boolean { | |
| return this.enableHooks; | |
| } | |
| getEnableHooksUI(): boolean { | |
| return this.enableHooksUI; | |
| } | |
| getGemmaModelRouterEnabled(): boolean { | |
| return this.gemmaModelRouter.enabled ?? false; | |
| } | |
| getGemmaModelRouterSettings(): GemmaModelRouterSettings { | |
| return this.gemmaModelRouter; | |
| } | |
| getAgentSessionNoninteractiveEnabled(): boolean { | |
| return ( | |
| process.env['GEMINI_CLI_EXP_AGENT'] === 'true' || | |
| this.agentSessionNoninteractiveEnabled | |
| ); | |
| } | |
| getAgentSessionInteractiveEnabled(): boolean { | |
| return ( | |
| process.env['GEMINI_CLI_EXP_AGENT'] === 'true' || | |
| this.agentSessionInteractiveEnabled | |
| ); | |
| } | |
| /** | |
| * Get override settings for a specific agent. | |
| * Reads from agents.overrides.<agentName>. | |
| */ | |
| getAgentOverride(agentName: string): AgentOverride | undefined { | |
| return this.getAgentsSettings()?.overrides?.[agentName]; | |
| } | |
| /** | |
| * Get browser agent configuration. | |
| * Combines generic AgentOverride fields with browser-specific customConfig. | |
| * This is the canonical way to access browser agent settings. | |
| */ | |
| getBrowserAgentConfig(): { | |
| enabled: boolean; | |
| model?: string; | |
| customConfig: BrowserAgentCustomConfig; | |
| } { | |
| const override = this.getAgentOverride('browser_agent'); | |
| const customConfig = this.getAgentsSettings()?.browser ?? {}; | |
| return { | |
| enabled: override?.enabled ?? false, | |
| model: override?.modelConfig?.model, | |
| customConfig: { | |
| sessionMode: customConfig.sessionMode ?? 'persistent', | |
| headless: customConfig.headless ?? false, | |
| profilePath: customConfig.profilePath, | |
| visualModel: customConfig.visualModel, | |
| allowedDomains: customConfig.allowedDomains, | |
| disableUserInput: customConfig.disableUserInput, | |
| maxActionsPerTask: customConfig.maxActionsPerTask ?? 100, | |
| confirmSensitiveActions: customConfig.confirmSensitiveActions, | |
| blockFileUploads: customConfig.blockFileUploads, | |
| }, | |
| }; | |
| } | |
| /** | |
| * Determines if user input should be disabled during browser automation. | |
| * Based on the `disableUserInput` setting and `headless` mode. | |
| */ | |
| shouldDisableBrowserUserInput(): boolean { | |
| const browserConfig = this.getBrowserAgentConfig(); | |
| return ( | |
| browserConfig.customConfig?.disableUserInput !== false && | |
| !browserConfig.customConfig?.headless | |
| ); | |
| } | |
| async createToolRegistry(): Promise<ToolRegistry> { | |
| const registry = new ToolRegistry( | |
| this, | |
| this.messageBus, | |
| /* isMainRegistry= */ true, | |
| ); | |
| // helper to create & register core tools that are enabled | |
| const maybeRegister = ( | |
| toolClass: { name: string; Name?: string }, | |
| registerFn: () => void, | |
| ) => { | |
| const className = toolClass.name; | |
| const toolName = toolClass.Name || className; | |
| const coreTools = this.getCoreTools(); | |
| // On some platforms, the className can be minified to _ClassName. | |
| const normalizedClassName = className.replace(/^_+/, ''); | |
| let isEnabled = true; // Enabled by default if coreTools is not set. | |
| if (coreTools) { | |
| isEnabled = coreTools.some( | |
| (tool) => | |
| tool === toolName || | |
| tool === normalizedClassName || | |
| tool.startsWith(`${toolName}(`) || | |
| tool.startsWith(`${normalizedClassName}(`), | |
| ); | |
| } | |
| if (isEnabled) { | |
| registerFn(); | |
| } | |
| }; | |
| maybeRegister(UpdateTopicTool, () => | |
| registry.registerTool(new UpdateTopicTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(LSTool, () => | |
| registry.registerTool(new LSTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(ReadFileTool, () => | |
| registry.registerTool(new ReadFileTool(this, this.messageBus)), | |
| ); | |
| if (this.getUseRipgrep()) { | |
| let useRipgrep = false; | |
| let errorString: undefined | string = undefined; | |
| try { | |
| useRipgrep = await this.canUseRipgrep(); | |
| } catch (error: unknown) { | |
| errorString = String(error); | |
| } | |
| if (useRipgrep) { | |
| maybeRegister(RipGrepTool, () => | |
| registry.registerTool(new RipGrepTool(this, this.messageBus)), | |
| ); | |
| } else { | |
| debugLogger.warn(`Ripgrep is not available. Falling back to GrepTool.`); | |
| logRipgrepFallback(this, new RipgrepFallbackEvent(errorString)); | |
| maybeRegister(GrepTool, () => | |
| registry.registerTool(new GrepTool(this, this.messageBus)), | |
| ); | |
| } | |
| } else { | |
| maybeRegister(GrepTool, () => | |
| registry.registerTool(new GrepTool(this, this.messageBus)), | |
| ); | |
| } | |
| maybeRegister(GlobTool, () => | |
| registry.registerTool(new GlobTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(ActivateSkillTool, () => | |
| registry.registerTool(new ActivateSkillTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(EditTool, () => | |
| registry.registerTool(new EditTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(WriteFileTool, () => | |
| registry.registerTool(new WriteFileTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(WebFetchTool, () => | |
| registry.registerTool(new WebFetchTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(ReadMcpResourceTool, () => | |
| registry.registerTool(new ReadMcpResourceTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(ListMcpResourcesTool, () => | |
| registry.registerTool(new ListMcpResourcesTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(ShellTool, () => | |
| registry.registerTool(new ShellTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(ListBackgroundProcessesTool, () => | |
| registry.registerTool( | |
| new ListBackgroundProcessesTool(this, this.messageBus), | |
| ), | |
| ); | |
| maybeRegister(ReadBackgroundOutputTool, () => | |
| registry.registerTool( | |
| new ReadBackgroundOutputTool(this, this.messageBus), | |
| ), | |
| ); | |
| maybeRegister(WebSearchTool, () => | |
| registry.registerTool(new WebSearchTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(AskUserTool, () => | |
| registry.registerTool(new AskUserTool(this.messageBus)), | |
| ); | |
| if (this.getUseWriteTodos()) { | |
| maybeRegister(WriteTodosTool, () => | |
| registry.registerTool(new WriteTodosTool(this.messageBus)), | |
| ); | |
| } | |
| if (this.isPlanEnabled()) { | |
| maybeRegister(ExitPlanModeTool, () => | |
| registry.registerTool(new ExitPlanModeTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(EnterPlanModeTool, () => | |
| registry.registerTool(new EnterPlanModeTool(this, this.messageBus)), | |
| ); | |
| } | |
| if (this.isTrackerEnabled()) { | |
| maybeRegister(TrackerCreateTaskTool, () => | |
| registry.registerTool(new TrackerCreateTaskTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(TrackerUpdateTaskTool, () => | |
| registry.registerTool(new TrackerUpdateTaskTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(TrackerGetTaskTool, () => | |
| registry.registerTool(new TrackerGetTaskTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(TrackerListTasksTool, () => | |
| registry.registerTool(new TrackerListTasksTool(this, this.messageBus)), | |
| ); | |
| maybeRegister(TrackerAddDependencyTool, () => | |
| registry.registerTool( | |
| new TrackerAddDependencyTool(this, this.messageBus), | |
| ), | |
| ); | |
| maybeRegister(TrackerVisualizeTool, () => | |
| registry.registerTool(new TrackerVisualizeTool(this, this.messageBus)), | |
| ); | |
| } | |
| // Register Subagent Tool | |
| maybeRegister(AgentTool, () => | |
| registry.registerTool(new AgentTool(this, this.messageBus)), | |
| ); | |
| await registry.discoverAllTools(); | |
| registry.sortTools(); | |
| return registry; | |
| } | |
| /** | |
| * Get the hook system instance | |
| */ | |
| getHookSystem(): HookSystem | undefined { | |
| return this.hookSystem; | |
| } | |
| /** | |
| * Get hooks configuration | |
| */ | |
| getHooks(): { [K in HookEventName]?: HookDefinition[] } | undefined { | |
| return this.hooks; | |
| } | |
| /** | |
| * Get project-specific hooks configuration | |
| */ | |
| getProjectHooks(): { [K in HookEventName]?: HookDefinition[] } | undefined { | |
| return this.projectHooks; | |
| } | |
| /** | |
| * Update the list of disabled hooks dynamically. | |
| * This is used to keep the running system in sync with settings changes | |
| * without risk of loading new hook definitions into memory. | |
| */ | |
| updateDisabledHooks(disabledHooks: string[]): void { | |
| this.disabledHooks = disabledHooks; | |
| } | |
| /** | |
| * Get disabled hooks list | |
| */ | |
| getDisabledHooks(): string[] { | |
| return this.disabledHooks; | |
| } | |
| /** | |
| * Get experiments configuration | |
| */ | |
| getExperiments(): Experiments | undefined { | |
| return this.experiments; | |
| } | |
| /** | |
| * Set experiments configuration | |
| */ | |
| setExperiments(experiments: Experiments): void { | |
| this.experiments = experiments; | |
| const flagSummaries = Object.entries(experiments.flags ?? {}) | |
| .sort(([a], [b]) => a.localeCompare(b)) | |
| .map(([flagId, flag]) => { | |
| const summary: Record<string, unknown> = { flagId }; | |
| if (flag.boolValue !== undefined) { | |
| summary['boolValue'] = flag.boolValue; | |
| } | |
| if (flag.floatValue !== undefined) { | |
| summary['floatValue'] = flag.floatValue; | |
| } | |
| if (flag.intValue !== undefined) { | |
| summary['intValue'] = flag.intValue; | |
| } | |
| if (flag.stringValue !== undefined) { | |
| summary['stringValue'] = flag.stringValue; | |
| } | |
| const int32Length = flag.int32ListValue?.values?.length ?? 0; | |
| if (int32Length > 0) { | |
| summary['int32ListLength'] = int32Length; | |
| } | |
| const stringListLength = flag.stringListValue?.values?.length ?? 0; | |
| if (stringListLength > 0) { | |
| summary['stringListLength'] = stringListLength; | |
| } | |
| return summary; | |
| }); | |
| const summary = { | |
| experimentIds: experiments.experimentIds ?? [], | |
| flags: flagSummaries, | |
| }; | |
| const summaryString = inspect(summary, { | |
| depth: null, | |
| maxArrayLength: null, | |
| maxStringLength: null, | |
| breakLength: 80, | |
| compact: false, | |
| }); | |
| debugLogger.debug('Experiments loaded', summaryString); | |
| } | |
| private onAgentsRefreshed = async () => { | |
| // Propagate updates to the active chat session | |
| const client = this.geminiClient; | |
| if (client?.isInitialized()) { | |
| await client.setTools(); | |
| client.updateSystemInstruction(); | |
| } else { | |
| debugLogger.debug( | |
| '[Config] GeminiClient not initialized; skipping live prompt/tool refresh.', | |
| ); | |
| } | |
| }; | |
| /** | |
| * Disposes of resources and removes event listeners. | |
| */ | |
| async dispose(): Promise<void> { | |
| this.logCurrentModeDuration(this.getApprovalMode()); | |
| coreEvents.off(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed); | |
| this.agentRegistry?.dispose(); | |
| this.geminiClient?.dispose(); | |
| if (this.mcpClientManager) { | |
| await this.mcpClientManager.stop(); | |
| } | |
| } | |
| } | |
| // Export model constants for use in CLI | |
| export { DEFAULT_GEMINI_FLASH_MODEL }; | |