Compare commits

..

235 Commits

Author SHA1 Message Date
mkorwel ad14cd4466 fix(cli): add back github shorthand support 2026-03-19 16:36:36 -07:00
mkorwel 01e937fedb fix(cli): ensure git clone is used when --ref is provided 2026-03-19 16:36:28 -07:00
mkorwel bedb8552a8 fix(workspaces): use ${extensionPath} for absolute script paths in skill 2026-03-19 15:58:27 -07:00
mkorwel a03eb159e4 chore: remove old location of gemini-extension.json 2026-03-19 15:56:50 -07:00
mkorwel fc5e289f81 fix(workspaces): move gemini-extension.json back to root for git install 2026-03-19 15:53:09 -07:00
mkorwel b3848043f1 revert: remove github shorthand fix from this branch 2026-03-19 15:37:28 -07:00
mkorwel 400a0020e2 fix(workspaces): correct extension structure and portable script paths 2026-03-19 15:32:55 -07:00
mkorwel c9cc49f62f fix(cli): recognize github: and gitlab: shorthand in extension install 2026-03-19 15:20:16 -07:00
mkorwel f380aedbf8 fix(cli): move default query command to end of yargs chain to prevent subcommand shadowing 2026-03-19 11:54:53 -07:00
mkorwel 8d9c7b90e9 fix(workspaces): update setup policy sync path 2026-03-19 10:19:07 -07:00
mkorwel 0218d885a1 fix(workspaces): move skill and policy to root for extension discovery 2026-03-19 10:13:12 -07:00
mkorwel df2ac184dd feat(workspaces): transform workspaces feature into a distributable extension 2026-03-19 09:53:51 -07:00
mkorwel 8cdd22d27d feat(workspaces): unify host and container paths in startup script 2026-03-19 08:07:34 -07:00
mkorwel 98913642cf feat(workspaces): use GIT_CONFIG env vars to bypass safe.directory on read-only FS 2026-03-19 01:16:22 -07:00
mkorwel bb1137d0fa feat(workspaces): enforce UID 1000 ownership and use sudo for host git ops 2026-03-19 01:15:10 -07:00
mkorwel 4591d4177a feat(workspaces): dynamically detect remote user for git operations 2026-03-19 01:14:23 -07:00
mkorwel b8343150bb feat(workspaces): unify paths and remove redundant worktree repair 2026-03-19 01:07:38 -07:00
mkorwel 80a54318ec feat(workspaces): revert shell to global gemini command 2026-03-19 01:00:20 -07:00
mkorwel f575231ab4 feat(workspaces): use local binary for shell and fix iTerm2 quoting 2026-03-19 00:58:45 -07:00
mkorwel a3350a0ade feat(workspaces): silence auto-updates and improve sandbox/host info in footer 2026-03-19 00:55:49 -07:00
mkorwel c33c128568 feat(workspaces): hide tmux footer and inject local settings into remote worktree 2026-03-19 00:52:49 -07:00
mkorwel 368b80d461 fix(workspaces): restore authEnv variable in orchestrator 2026-03-19 00:50:32 -07:00
mkorwel 8ffa185949 feat(workspaces): enable true color support in remote sessions 2026-03-19 00:49:24 -07:00
mkorwel 9bb8e702b9 feat(workspaces): sync local theme and UI settings to remote worker 2026-03-19 00:44:39 -07:00
mkorwel c2022d1651 fix(workspaces): disable folder trust and auto-updates on remote worker 2026-03-19 00:41:29 -07:00
mkorwel e82781ca7d scdript 2026-03-19 00:25:25 -07:00
mkorwel 7d11fdeb8c feat(workspaces): implement unified scripts/workspaces.ts entry point and simplify commands 2026-03-19 00:21:41 -07:00
mkorwel 0df26997b4 feat(workspaces): support --open flag for terminal target selection 2026-03-19 00:09:16 -07:00
mkorwel a8b48c0572 feat(workspaces): implement workspace:shell command for ad-hoc interactive sessions 2026-03-19 00:06:08 -07:00
mkorwel 864090313f feat(workspaces): mount gh config for secure authentication 2026-03-18 23:57:12 -07:00
mkorwel 63faf92440 feat(workspaces): implement remote worktree repair and .env injection 2026-03-18 23:56:35 -07:00
mkorwel 2f1175b9fe feat(workspaces): consolidate cleanup commands and add safety confirmations 2026-03-18 23:42:51 -07:00
mkorwel f42f366ca4 fix(workspaces): add workspace:prune alias for cleanup 2026-03-18 23:36:52 -07:00
mkorwel a4ba716281 feat(workspaces): include docker and image pruning in bulk cleanup 2026-03-18 23:32:59 -07:00
mkorwel ba824a1aba fix(workspaces): update cleanup paths for unified architecture 2026-03-18 23:31:52 -07:00
mkorwel 48fe995b55 feat(workspaces): darken tmux status bar and add pruning docs 2026-03-18 23:30:40 -07:00
mkorwel 8228b6518e feat(workspaces): customize tmux status bar for better UX and session ID 2026-03-18 23:27:20 -07:00
mkorwel 6414594a4c fix(workspaces): restore tmux for persistent sessions 2026-03-18 23:22:00 -07:00
mkorwel da36bb8d01 docs(workspaces): add npx tsx constraint to SKILL.md 2026-03-18 23:11:21 -07:00
mkorwel 4e19e9ac56 fix(workspaces): clean up setup prompt UX 2026-03-18 23:07:45 -07:00
mkorwel 8530083a98 fix(workspaces): link workspace:status directly to local script and clean up fleet.ts 2026-03-18 23:03:00 -07:00
mkorwel f3b4256812 fix(workspaces): correct remote path for fleet status 2026-03-18 22:58:57 -07:00
mkorwel 08d3646ace fix(workspaces): add missing frontmatter to SKILL.md for discovery 2026-03-18 22:06:46 -07:00
mkorwel dfbe583050 docs(workspaces): update onboarding to reflect unified architecture and npx usage 2026-03-18 21:57:27 -07:00
mkorwel 8cd24bf667 fix(workspaces): resolve auth and path issues for remote reviews 2026-03-18 21:52:41 -07:00
mkorwel c4c5d4e956 feat(workspaces): unify architecture and improve auth UX 2026-03-18 21:43:07 -07:00
mkorwel a1763a3e44 feat(workspaces): improve prompt UX for sensitive defaults and mask API keys 2026-03-18 13:41:47 -07:00
mkorwel 466867b593 feat(workspaces): implement intelligent authentication switching and restore sync logic 2026-03-18 13:38:50 -07:00
mkorwel 85dd66e749 fix(workspaces): resolve setup variable scope and initialization order issues 2026-03-18 12:47:05 -07:00
mkorwel 911466bd03 feat(workspaces): implement resume logic and environment-based auth for faster setup 2026-03-18 12:41:36 -07:00
mkorwel 15db44f9ec fix(workspaces): apply sudo to all remote sync calls in setup 2026-03-18 12:39:36 -07:00
mkorwel fd8755d292 fix(workspaces): implement sudo support for rsync to resolve remote permission issues 2026-03-18 12:35:46 -07:00
mkorwel 489b36d5b2 fix(workspaces): sync credentials into isolated config directory for remote auth 2026-03-18 12:32:35 -07:00
mkorwel 496a85f46b fix(workspaces): ensure container user owns configuration directories 2026-03-18 12:28:34 -07:00
mkorwel c6915fbfc1 fix(workspaces): add missing os import in setup.ts 2026-03-18 12:20:19 -07:00
mkorwel e23cbdc258 fix(workspaces): fix invalid gemini args and initialize remote auth settings 2026-03-18 12:16:59 -07:00
mkorwel f3a2457af3 feat(workspaces): ask for existing PAT before suggesting creation 2026-03-18 12:00:50 -07:00
mkorwel 93b58feb9b fix(workspaces): move git setup to host and fix path pluralization 2026-03-18 11:56:44 -07:00
mkorwel fe79087d7a perf(workspaces): use GraphQL for instantaneous fork discovery 2026-03-18 11:38:39 -07:00
mkorwel c63c0d0422 fix(workspaces): improve fork discovery using GitHub API for accurate identification 2026-03-18 11:32:29 -07:00
mkorwel c91e00e0ed feat(workspaces): implement interactive fork selection and management 2026-03-18 11:27:30 -07:00
mkorwel d20923953a feat(workspaces): add interactive fork creation to setup 2026-03-18 11:26:19 -07:00
mkorwel 0bf765bae0 feat(workspaces): implement foreground, background, and iterm2 UI targets 2026-03-18 11:23:29 -07:00
mkorwel d28188bf12 feat(workspaces): transform offload into repository-agnostic Gemini Workspaces 2026-03-18 11:18:53 -07:00
mkorwel 494425cdc4 feat(offload): implement phased, turn-key configuration and onboarding 2026-03-18 11:03:56 -07:00
mkorwel 5dfc91cca5 fix(offload): implement robust fork detection and refine boot disk allocation 2026-03-18 11:01:40 -07:00
mkorwel 32e3d3e68e fix(offload): add missing os import in GceCosProvider 2026-03-18 10:52:22 -07:00
mkorwel 82bf2e6d5f fix(offload): use metadata-from-file for robust startup script deployment 2026-03-18 10:50:33 -07:00
mkorwel 822f478d9b fix(offload): switch to global gemini binary for remote execution 2026-03-18 10:42:05 -07:00
mkorwel 7f8bd31b08 feat(offload): bundle full dev toolset including nightly gemini into image 2026-03-17 22:02:41 -07:00
mkorwel 0fac6b9ecf fix(offload): add binary validation logging to remote worker 2026-03-17 21:59:43 -07:00
mkorwel ae387866c5 fix(offload): increase entrypoint verbosity for easier debugging 2026-03-17 21:56:25 -07:00
mkorwel 51f1ebecbd fix(offload): resolve rsync permission errors and refine remote ownership 2026-03-17 21:48:23 -07:00
mkorwel e7d3860d40 fix(offload): use OSC 8 for magic link rendering to prevent line splitting 2026-03-17 21:44:34 -07:00
mkorwel cf86172c14 Merge remote-tracking branch 'origin/main' into mk-offload 2026-03-17 21:30:23 -07:00
mkorwel 16dd840215 fix(offload): correct script extensions for remote execution 2026-03-17 21:22:23 -07:00
Abhi be7c7bb83d fix(cli): resolve subagent grouping and UI state persistence (#22252) 2026-03-18 03:11:20 +00:00
AK 7bfe6ac418 feat(core): subagent local execution and tool isolation (#22718) 2026-03-18 02:34:44 +00:00
adithya32 bd34a42ec3 fix(cli): lazily load settings in onModelChange to prevent stale closure data loss (#20403)
Co-authored-by: Spencer <spencertang@google.com>
2026-03-18 00:40:38 +00:00
N. Taylor Mullen e2658ccda8 refactor(cli): remove extra newlines in ShellToolMessage.tsx (#22868)
Co-authored-by: Spencer <spencertang@google.com>
2026-03-17 23:48:16 +00:00
anj-s b8719bcd47 feat: add 'blocked' status to tasks and todos (#22735) 2026-03-17 23:24:26 +00:00
Sakshi semalti e1eefffcf1 fix(cli): automatically add all VSCode workspace folders to Gemini context (#21380)
Co-authored-by: Spencer <spencertang@google.com>
2026-03-17 23:05:49 +00:00
Alisa 7ae39fd622 feat(a2a): add agent acknowledgment command and enhance registry discovery (#22389) 2026-03-17 22:47:05 +00:00
mkorwel 70b3e7c786 fix(offload): restore path consistency with ~/.offload mounts 2026-03-17 15:34:13 -07:00
mkorwel bdf00c1d8c fix(offload): implement auto-update for stale containers 2026-03-17 15:25:39 -07:00
N. Taylor Mullen fb9264bf80 chore: add posttest build hooks and fix missing dependencies (#22865) 2026-03-17 22:23:00 +00:00
anj-s d4397dbfc5 feat(tracker): integrate task tracker protocol into core system prompt (#22442) 2026-03-17 22:19:36 +00:00
mkorwel ff56c33f64 feat(offload): add tmux and docker to maintainer image 2026-03-17 15:05:51 -07:00
Spencer 5fb0d1f01d fix(core): add actionable warnings for terminal fallbacks (#14426) (#22211) 2026-03-17 21:57:37 +00:00
mkorwel 934ee8baf7 fix(offload): implement self-healing container health checks and fortified startup 2026-03-17 14:50:29 -07:00
Abhi 95bca2c3b3 fix(cli): expand tilde in policy paths from settings.json (#22772) 2026-03-17 21:48:24 +00:00
mkorwel 35e358535c feat(offload): finalize verified single-command onboarding flow 2026-03-17 14:46:49 -07:00
matt korwel e0be1b2afd fix(cli): use active sessionId in useLogger and improve resume robustness (#22606) 2026-03-17 21:42:40 +00:00
gemini-cli-robot 5d4e4c2814 chore(release): bump version to 0.36.0-nightly.20260317.2f90b4653 (#22858) 2026-03-17 21:18:21 +00:00
kevinjwang1 27a50191e3 Add support for dynamic model Resolution to ModelConfigService (#22578) 2026-03-17 21:15:50 +00:00
mkorwel 38f1627e9b fix(offload): add sudo to docker commands for COS compatibility 2026-03-17 14:03:30 -07:00
Sandy Tao 77ca3c0e13 fix(devtools): use theme-aware text colors for console warnings and errors (#22181) 2026-03-17 21:00:40 +00:00
AK 2504105a1c feat(core): multi-registry architecture and tool filtering for subagents (#22712) 2026-03-17 20:54:07 +00:00
gemini-cli-robot a361a84708 Changelog for v0.34.0-preview.4 (#22752)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
Co-authored-by: Sam Roberts <158088236+g-samroberts@users.noreply.github.com>
2026-03-17 20:46:38 +00:00
mkorwel 0c3f836682 fix(offload): run persistent sessions inside the container 2026-03-17 13:36:39 -07:00
gemini-cli-robot ff196fbe6f Changelog for v0.33.2 (#22730)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
Co-authored-by: Sam Roberts <158088236+g-samroberts@users.noreply.github.com>
2026-03-17 20:33:30 +00:00
David Pierce 2f90b46537 Linux sandbox seccomp (#22815)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
2026-03-17 20:29:13 +00:00
mkorwel fe285a93c6 feat(offload): restore and enhance iTerm2 auto-launch UI 2026-03-17 13:23:15 -07:00
Sandy Tao 82d8680dcc refactor(core): align JIT memory placement with tiered context model (#22766) 2026-03-17 20:20:32 +00:00
Jomak-x 1f3f7247b1 fix(cli): clean up stale pasted placeholder metadata after word/line deletions (#20375)
Co-authored-by: ruomeng <ruomeng@google.com>
2026-03-17 20:16:26 +00:00
mkorwel e937681cd3 docs(offload): document persistence and fix status script orchestrator 2026-03-17 13:03:40 -07:00
Sandy Tao 69e2d8c7ae docs: distribute package-specific GEMINI.md context to each package (#22734) 2026-03-17 19:51:23 +00:00
Adib234 77a874cf65 feat(plan): add 'All the above' option to multi-select AskUser questions (#22365)
Co-authored-by: jacob314 <jacob314@gmail.com>
2026-03-17 19:17:34 +00:00
Sehoon Shon b211f30d95 fix(cli): override j/k navigation in settings dialog to fix search input conflict (#22800) 2026-03-17 19:08:45 +00:00
mkorwel bc9f0fbbd8 feat(offload): silence provisioning warnings and add --yes flag for setup 2026-03-17 10:28:18 -07:00
mkorwel 9c027a7924 feat(offload): unify onboarding flow and improve logging UX 2026-03-17 10:26:59 -07:00
mkorwel 7d3a856a1a feat(offload): finalize e2e automation and corporate routing fixes 2026-03-17 10:14:27 -07:00
Sehoon Shon fc51e50bc6 fix(core): handle surrogate pairs in truncateString (#22754) 2026-03-17 05:41:19 +00:00
AK 695bcaea0d feat(core): add foundation for subagent tool isolation (#22708) 2026-03-17 03:54:33 +00:00
mkorwel 62b9b03962 docs(offload): document network research and final configuration 2026-03-16 20:03:42 -07:00
gemini-cli-robot abe83fce0b Changelog for v0.34.0-preview.3 (#22393)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
Co-authored-by: Sam Roberts <158088236+g-samroberts@users.noreply.github.com>
2026-03-17 00:52:17 +00:00
Adib234 61fd71dc29 fix(plan): allowlist get_internal_docs in Plan Mode (#22668) 2026-03-17 00:34:30 +00:00
Kevin Ramdass cddd9e7148 fix(config): ensure discoveryMaxDirs is passed to global config during initialization (#22744)
Co-authored-by: matt korwel <matt.korwel@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-16 23:38:56 +00:00
mkorwel 7c66e7e427 chore(offload): organize configuration into .gemini/offload sub-folder 2026-03-16 15:48:11 -07:00
mkorwel 0ee5d0823e feat(offload): make system project-agnostic and update docs 2026-03-16 15:42:23 -07:00
mkorwel a77f1f5e24 docs(offload): update maintainer onboarding for provider architecture 2026-03-16 15:40:59 -07:00
mkorwel cc7b7e2fbc feat(offload): implement WorkerProvider abstraction and robust IAP fallback 2026-03-16 15:33:40 -07:00
Sandy Tao 30271d64eb feat(config): enable JIT context loading by default (#22736) 2026-03-16 22:12:28 +00:00
Adam Weidman 605432ea70 refactor(core): replace positional execute params with ExecuteOptions bag (#22674) 2026-03-16 21:50:24 +00:00
Aishanee Shah 990d010ecf feat(core): implement Stage 2 security and consistency improvements for web_fetch (#22217) 2026-03-16 21:38:53 +00:00
Bryan Morgan b6c6da3618 feat(core): increase thought signature retry resilience (#22202)
Co-authored-by: Aishanee Shah <aishaneeshah@google.com>
2026-03-16 21:35:33 +00:00
David Pierce 8f22ffd2b1 Linux sandbox bubblewrap (#22680) 2026-03-16 21:34:48 +00:00
Adam Weidman 44ce90d76c refactor(core): introduce InjectionService with source-aware injection and backend-native background completions (#22544) 2026-03-16 21:06:29 +00:00
Sandy Tao b91f75cd6d fix(core): fix three JIT context bugs in read_file, read_many_files, and memoryDiscovery (#22679) 2026-03-16 20:10:50 +00:00
gemini-cli-robot dfe22aae21 Changelog for v0.34.0-preview.2 (#22220)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-03-16 19:22:01 +00:00
anj-s bba9c07541 feat(tracker): polish UI sorting and formatting (#22437) 2026-03-16 19:18:01 +00:00
Emily Hedlund 05fda0cf01 feat(extensions): implement cryptographic integrity verification for extension updates (#21772) 2026-03-16 19:01:52 +00:00
Abhi d43ec6c8f3 feat: enable subagents (#22386) 2026-03-16 18:40:12 +00:00
Jack Wotherspoon 56e0865a7b docs(changelog): remove internal commands from release notes (#22529) 2026-03-16 18:39:00 +00:00
mkorwel 5786dc675d fix(offload): apply empirically verified nic0 hostname construction to setup 2026-03-16 11:23:51 -07:00
mkorwel e4db5931f4 fix(offload): ensure leading dot in DNS suffix for project isolation 2026-03-16 11:19:13 -07:00
Michael Bleigh cd2096ca80 refactor(core): Creates AgentSession abstraction for consolidated agent interface. (#22270) 2026-03-16 17:59:02 +00:00
Sehoon Shon 48130ebd25 Guard pro model usage (#22665) 2026-03-16 17:44:25 +00:00
Christian Gunderman ef5627eece Disallow Object.create() and reflect. (#22408) 2026-03-16 16:24:27 +00:00
Sri Pasumarthi e3df87cf1a fix: Adjust ToolGroupMessage filtering to hide Confirming and show Canceled tool calls. (#22230) 2026-03-16 15:50:11 +00:00
Emily Hedlund 8bad5823a9 fix(a2a-server): resolve unsafe assignment lint errors (#22661) 2026-03-16 15:28:35 +00:00
kawasin73 fd62938945 docs(policy): remove trailing space from commandPrefix examples (#22264)
Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
2026-03-16 14:45:30 +00:00
anj-s fad032d466 perf: optimize TrackerService dependency checks (#22384) 2026-03-16 13:40:58 +00:00
cynthialong0-0 366aa84395 feat(agent): replace the runtime npx for browser agent chrome devtool mcp with pre-built bundle (#22213)
Co-authored-by: Gaurav Ghosh <gaghosh@google.com>
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
2026-03-16 08:05:38 +00:00
mkorwel 3b95667e33 fix(offload): restore high-performance direct internal routing with nic0 prefix 2026-03-16 00:16:26 -07:00
mkorwel 959bb5dc9b fix(offload): add pre-sync connectivity check to trigger SSO prompts 2026-03-15 23:49:10 -07:00
mkorwel 6d81c5ece1 fix(offload): ensure leading dot in DNS suffix and use absolute container paths 2026-03-15 23:47:41 -07:00
mkorwel 31b50f9413 fix(offload): resolve ReferenceError and harden setup SSH commands 2026-03-15 23:46:27 -07:00
mkorwel 2f8caea6a4 feat(offload): implement project-isolated SSH configuration for cleaner orchestration 2026-03-15 23:44:54 -07:00
mkorwel 0edc2623a4 fix(offload): temporarily disable fork management for streamlined setup 2026-03-15 23:27:27 -07:00
mkorwel 4d791904d4 fix(offload): resolve syntax error and finalize setup script 2026-03-15 20:17:23 -07:00
mkorwel de50780ab9 fix(offload): corrected github token magic link and instructions 2026-03-15 20:15:47 -07:00
mkorwel 826f66e25b fix(offload): prevent branch/config bleeding and restore missing rsync logic 2026-03-15 18:37:56 -07:00
mkorwel 1ae0ff0508 fix(offload): finalize fork discovery and Magic Link formatting for clean setup 2026-03-15 16:41:08 -07:00
mkorwel aa6c1625c1 fix(offload): improve fork detection logic and Magic Link clickability 2026-03-15 16:31:22 -07:00
mkorwel b9b94e57ca feat(offload): implement high-performance internal routing with IAP fallback 2026-03-15 16:20:31 -07:00
mkorwel bb36fc4486 feat(offload): implement internal-only network architecture and disable external IPs 2026-03-15 13:18:15 -07:00
mkorwel 7b9a8294ee feat(offload): inject SSH keys and add disk expansion logic to provisioning 2026-03-15 13:16:28 -07:00
Bryan Morgan 17b37144a9 fix(automation): evaluate staleness before checking protected labels (#22561) 2026-03-15 15:50:19 -04:00
mkorwel fb4c413412 feat(offload): rename container to maintainer-worker and finalize COS integration 2026-03-15 12:36:48 -07:00
mkorwel db5a00489a feat(offload): restore Invisible VM architecture using Container-Optimized OS and high-performance disk 2026-03-15 12:33:45 -07:00
mkorwel edc8bfdfe4 feat(offload): address provisioning warnings and implement high-performance startup-script container 2026-03-15 12:32:22 -07:00
mkorwel b5c1b9f77b docs(offload): move architectural rationale to user-facing documentation 2026-03-15 12:13:11 -07:00
mkorwel fb8ec967c1 feat(offload): update status command for container-aware job discovery 2026-03-15 12:09:59 -07:00
mkorwel e2ad7f4ecb feat(offload): implement persistent container workstation model 2026-03-15 12:07:50 -07:00
mkorwel 0a99550c6d feat(offload): re-center on persistent workstation model and archive container logic 2026-03-15 12:06:33 -07:00
mkorwel 084eee061d feat(offload): implement Isolation 2.0 with ephemeral containers and surgical RO/RW mounts 2026-03-15 12:03:38 -07:00
Bryan Morgan 5ddb517593 fix(automation): harden stale PR closer permissions and maintainer detection (#22558) 2026-03-15 14:58:56 -04:00
Abhi abd9e23337 fix(core): skip discovery for incomplete MCP configs and resolve merge race condition (#22494) 2026-03-15 18:28:26 +00:00
mkorwel b7db77e938 feat(offload): unify host/container paths and mounts for seamless orchestration 2026-03-15 10:45:59 -07:00
mkorwel 4d063015eb feat(offload): implement full container-native support with COS and docker exec 2026-03-15 10:42:07 -07:00
mkorwel 71c5b807a3 feat(build): implement two-trigger strategy for maintainer image builds 2026-03-15 10:01:46 -07:00
mkorwel 5f26de9eae docs(offload): implement maintainer onboarding guide and link from contributing 2026-03-15 09:54:27 -07:00
mkorwel e7cf6a4baa docs(offload): mark Phase 1 as verified in mission dossier 2026-03-15 09:45:32 -07:00
mkorwel 3a0c5eef9c feat(build): implement dual-tagging strategy and update next mission dossier 2026-03-15 09:41:50 -07:00
mkorwel 3a813171f9 feat(build): isolate maintainer build logic to a dedicated pipeline file 2026-03-15 09:40:05 -07:00
mkorwel f7837d4d42 feat(build): decouple maintainer image into dedicated Dockerfile 2026-03-15 09:17:54 -07:00
mkorwel f7d5510b8b feat(build): implement multi-stage maintainer parent image and dual-tag cloud build 2026-03-15 09:16:01 -07:00
Abhi 6061d8cac7 fix(core): merge user settings with extension-provided MCP servers (#22484) 2026-03-15 02:46:06 +00:00
N. Taylor Mullen bd590bbde6 fix(cli): improve command conflict handling for skills (#21942) 2026-03-14 23:33:14 +00:00
kevinjwang1 0bf7ea60c5 Add ModelDefinitions to ModelConfigService (#22302) 2026-03-14 21:45:21 +00:00
kevinjwang1 8f2697c2e5 Add registry var (#22224) 2026-03-14 20:14:51 +00:00
Abhi c5502b2dc5 fix(core): fix manual deletion of subagent histories (#22407) 2026-03-14 20:09:43 +00:00
mkorwel 162ac93f81 feat(offload): implement continuous golden image pipeline and robust fork discovery 2026-03-14 11:59:41 -07:00
mkorwel 369241d271 feat(offload): implement unified control plane with log tailing and flexible attachment 2026-03-14 11:02:27 -07:00
mkorwel 7ee56e7cfd feat(offload): finalize high-performance architecture with fast-path SSH and worktrees 2026-03-14 09:23:40 -07:00
mkorwel b13fe22582 feat(offload): implement dynamic remote home resolution for reliable rsync 2026-03-14 01:47:05 -07:00
mkorwel af9dc5be58 feat(offload): refine for OS Login and add stop command 2026-03-14 01:31:15 -07:00
mkorwel fc1b5c0247 feat(offload): remove auto-deletion and transition to persistent one-worker model 2026-03-14 00:55:18 -07:00
mkorwel 1f14f609ee feat(offload): finalize GCE-only architecture and secure fleet management 2026-03-14 00:43:53 -07:00
Abhijit Balaji 9f7691fd88 feat(prompts): implement Topic-Action-Summary model for verbosity reduction (#21503) 2026-03-14 05:10:30 +00:00
Sandy Tao 3682842a5d fix(core): deduplicate project memory when JIT context is enabled (#22234) 2026-03-14 04:34:53 +00:00
mkorwel 45902f5e8a feat(offload): make setup script dynamic based on remote state 2026-03-13 20:08:30 -07:00
mkorwel 12824cb21c feat(policy): remove gh pr review from allow list to ensure manual confirmation 2026-03-13 20:00:16 -07:00
mkorwel cd1699fcda feat(offload): remove browser opener and make review-pr actionable 2026-03-13 19:56:52 -07:00
mkorwel b545670bbc docs: overhaul offload README with 'why' context and add skill-specific guides 2026-03-13 19:50:13 -07:00
mkorwel d4a386c4ed chore: final cleanup of offload configuration paths and test assertions 2026-03-13 19:47:02 -07:00
mkorwel 7113c5093d docs: remove future roadmap section from offload README 2026-03-13 19:41:06 -07:00
mkorwel 295831105b feat: add 'review-pr' skill and sunset 'async-pr-review' 2026-03-13 19:39:41 -07:00
mkorwel 8692b347f0 feat(offload): modularize playbooks with TaskRunner and integrate agentic fix-pr loop 2026-03-13 19:31:32 -07:00
mkorwel 3649059a28 feat: add 'fix-pr' skill for agentic iterative PR fixing 2026-03-13 19:25:03 -07:00
mkorwel 7d85e84d17 refactor: rename deep-review to offload and generalize capabilities 2026-03-13 19:03:30 -07:00
mkorwel f7ec958a69 feat(deep-review): finalize orchestration tests and integrate into main test suite 2026-03-13 18:51:48 -07:00
mkorwel 879ad72390 feat(deep-review): add tmux check to setup and expand test suite 2026-03-13 18:48:02 -07:00
mkorwel ee4fb24004 test(deep-review): add comprehensive orchestration tests and refactor for testability 2026-03-13 18:45:06 -07:00
mkorwel d1aba65cb0 feat(deep-review): allow choice between pre-existing and isolated instances for Gemini and GH CLI 2026-03-13 18:40:14 -07:00
Christian Gunderman 64c50d32ac Fix issue where config was undefined. (#22397) 2026-03-14 01:36:25 +00:00
mkorwel 824225bfa3 feat(deep-review): improve setup script robustness and quoting 2026-03-13 18:05:46 -07:00
mkorwel 187f2afb62 refactor: remove redundant shell scripts in favor of Node-based orchestration 2026-03-13 18:04:17 -07:00
mkorwel d5434f21d5 docs: clarify setup with pre-existing environments and isolation benefits 2026-03-13 17:57:00 -07:00
mkorwel 350f4dcd5c docs: add 'why' section to deep-review README 2026-03-13 17:52:10 -07:00
mkorwel ddc1522501 docs: move deep-review guide to skill folder as README 2026-03-13 17:51:31 -07:00
mkorwel 1f8e0a3d2e docs: add documentation for deep-review maintainer skill 2026-03-13 17:49:35 -07:00
mkorwel 0de1fb3f62 feat: add deep-review maintainer skill with node-based orchestration 2026-03-13 17:41:05 -07:00
anj-s 604d4ded8d fix(core): use session-specific temp directory for task tracker (#22382) 2026-03-13 23:35:26 +00:00
Gal Zahavi f75bdba568 fix(mcp): handle equivalent root resource URLs in OAuth validation (#20231) 2026-03-13 23:32:40 +00:00
Sehoon Shon 8d68ece8d6 Revert "fix(cli): validate --model argument at startup" (#22378) 2026-03-13 23:17:29 +00:00
Gal Zahavi b49fc8122d feat(security): add disableAlwaysAllow setting to disable auto-approvals (#21941) 2026-03-13 23:02:09 +00:00
anj-s b0d151bd65 feat(tracker): add tracker policy (#22379) 2026-03-13 22:19:04 +00:00
Christian Gunderman fe8d93c75a Promote stable tests. (#22253) 2026-03-13 21:32:00 +00:00
Tommaso Sciortino 24933a90d0 fix(cli): support CJK input and full Unicode scalar values in terminal protocols (#22353) 2026-03-13 21:24:26 +00:00
Gal Zahavi fa024133e6 feat(core): integrate SandboxManager to sandbox all process-spawning tools (#22231) 2026-03-13 21:11:51 +00:00
christine betts 24adacdbc2 Move keychain fallback to keychain service (#22332) 2026-03-13 20:57:08 +00:00
gemini-cli-robot aa23da67af chore/release: bump version to 0.35.0-nightly.20260313.bb060d7a9 (#22251) 2026-03-13 20:33:16 +00:00
cynthialong0-0 bfbd3c40a7 feat(agent): add allowed domain restrictions for browser agent (#21775) 2026-03-13 19:41:40 +00:00
anj-s dd8d4c98b3 feat(tracker): return TodoList display for tracker tools (#22060) 2026-03-13 18:18:33 +00:00
Gaurav d368997ca3 test: add Object.create context regression test and tool confirmation integration test (#22356) 2026-03-13 17:49:33 +00:00
Abhi bbd80c9393 docs: overhaul subagents documentation and add /agents command (#22345) 2026-03-13 17:26:13 +00:00
Abhi 3b601b3d90 refactor(ui): extract SessionBrowser static ui components (#22348) 2026-03-13 17:25:13 +00:00
Adam Weidman b4bcd1a015 docs(core): add authentication guide for remote subagents (#22178) 2026-03-13 16:48:21 +00:00
Alexander Farber aa000d7d30 fix(core): show descriptive error messages when saving settings fails (#18095)
Co-authored-by: Dev Randalpura <devrandalpura@google.com>
2026-03-13 16:19:56 +00:00
Tommaso Sciortino 2a7e602356 refactor(cli): consolidate getErrorMessage utility to core (#22190) 2026-03-13 15:40:29 +00:00
matt korwel 8d0b2d7f1b feat(skills): improve async-pr-review workflow and logging (#21790) 2026-03-13 15:18:07 +00:00
Ankit c156bac5f7 fix(settings): prevent j/k navigation keys from intercepting edit buffer input (#21865) 2026-03-13 14:55:36 +00:00
Adib234 263b8cd3b3 fix(plan): Fix AskUser evals (#22074) 2026-03-13 13:30:19 +00:00
360 changed files with 16993 additions and 3301 deletions
+54
View File
@@ -0,0 +1,54 @@
# --- STAGE 1: Base Runtime ---
FROM docker.io/library/node:20-slim AS base
ARG CLI_VERSION_ARG
ENV CLI_VERSION=$CLI_VERSION_ARG
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
curl \
dnsutils \
less \
jq \
ca-certificates \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# --- STAGE 2: Maintainer ---
FROM base AS maintainer
# Install "Maintainer Bloat" - tools needed for development and offloading
RUN apt-get update && apt-get install -y --no-install-recommends \
make \
g++ \
gh \
git \
unzip \
rsync \
ripgrep \
procps \
psmisc \
lsof \
socat \
tmux \
docker.io \
build-essential \
libsecret-1-dev \
libkrb5-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install global dev tools
RUN npm install -g tsx vitest @google/gemini-cli@nightly prettier eslint typescript cross-env
# Set up npm global package folder
RUN mkdir -p /usr/local/share/npm-global \
&& chown -R node:node /usr/local/share/npm-global
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
# Switch to non-root user node
USER node
# Default entrypoint
CMD ["/bin/bash"]
+50
View File
@@ -0,0 +1,50 @@
steps:
# Step 1: Install root dependencies
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Install Dependencies'
entrypoint: 'npm'
args: ['install']
# Step 2: Authenticate for Docker
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Authenticate docker'
entrypoint: 'npm'
args: ['run', 'auth']
# Step 3: Build workspace packages
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Build packages'
entrypoint: 'npm'
args: ['run', 'build:packages']
# Step 4: Build Maintainer Image
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
id: 'Build Maintainer Image'
entrypoint: 'bash'
args:
- '-c'
- |-
IMAGE_BASE="us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer"
# Determine the primary tag (branch name or 'latest' for main)
RAW_BRANCH="${_HEAD_BRANCH:-${BRANCH_NAME}}"
if [ "$${RAW_BRANCH}" == "main" ]; then
TAG_PRIMARY="latest"
else
TAG_PRIMARY=$(echo "$${RAW_BRANCH}" | sed 's/[^a-zA-Z0-9]/-/g' | tr '[:upper:]' '[:lower:]')
fi
TAG_SHA="${SHORT_SHA}"
echo "📦 Building Maintainer Image for: $${RAW_BRANCH} -> $${TAG_PRIMARY}"
docker build -f .gcp/Dockerfile.maintainer \
-t "$${IMAGE_BASE}:$${TAG_SHA}" \
-t "$${IMAGE_BASE}:$${TAG_PRIMARY}" .
docker push "$${IMAGE_BASE}:$${TAG_SHA}"
docker push "$${IMAGE_BASE}:$${TAG_PRIMARY}"
options:
defaultLogsBucketBehavior: 'REGIONAL_USER_OWNED_BUCKET'
dynamicSubstitutions: true
+31
View File
@@ -0,0 +1,31 @@
# Fix PR Skill
The `fix-pr` skill enables the Gemini CLI agent to autonomously and iteratively
fix a pull request until it is mergeable and passes all quality checks.
## When to use fix-pr
- **Autonomous fixing**: Move a PR from failing to green without manual
intervention.
- **Iterative loops**: Automatically handle the "fix -> push -> wait for CI"
cycle.
- **Context preservation**: Maintain context across multiple fix attempts.
## Sync vs Offload
This skill is designed to be versatile:
- **Synchronous mode**: Activate the skill in your current terminal for
quick fixes or conflict resolution.
- **Offloaded mode**: Use `npm run offload <PR> fix` to launch an iterative
fix loop on a remote workstation, keeping your local machine free.
## Objective
The goal is to move a PR from its current state (failing tests, merge conflicts,
or unaddressed comments) to a "Ready to Merge" state.
## Technical details
The skill uses the `gh` CLI and a specialized `wait-for-ci` utility to monitor
remote status. It is governed by the project's standard security policies.
+60
View File
@@ -0,0 +1,60 @@
# Fix PR Skill
This skill enables the agent to autonomously and iteratively fix a Pull Request until it is mergeable and passes all quality checks. It is designed to be run in a dedicated, non-blocking environment (like an offloaded remote session).
## Objective
The goal is to move a PR from its current state (failing tests, merge conflicts, or unaddressed comments) to a "Ready to Merge" state.
## Iterative Fix Loop
The agent must follow this loop until the PR is green and mergeable:
### 1. Synchronization & Merge Conflicts
- Fetch latest main: `git fetch origin main`.
- Attempt to merge: `git merge origin/main`.
- **If conflicts occur**:
- Use `git status` to identify conflicted files.
- Resolve the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) based on the project's architectural direction.
- `git add` and `git commit` the resolution.
### 2. CI Failure Analysis
- Check current status: `gh pr checks`.
- **If CI is failing**:
- Download failing logs: `gh run view --log-failed`.
- Analyze the specific error (lint, typecheck, or test failure).
- Reproduce the failure locally (e.g., `npm run lint`, `npm test -- <path>`).
- Apply a targeted fix.
### 3. Comment Resolution
- Fetch all comments: Use `scripts/fetch-pr-info.js` or `gh pr view --json reviews,comments`.
- Analyze:
- **Line-level comments**: Fix the specific logic requested.
- **Review rejections**: Address the high-level architectural concerns.
- **General comments**: Respond or implement requested changes.
- For each fixed item, summarize the change in a commit message.
### 4. Verification & Push
- Verify the fix locally: `npm run build` and relevant tests.
- **If local verification fails**: Re-analyze and apply a new fix.
- **If local verification passes**:
- `git add .`
- `git commit -m "fix: address <issue/comment>"`
- `git push origin HEAD`
### 5. Wait for CI
- Run the blocker tool: `npx tsx .gemini/skills/fix-pr/scripts/wait-for-ci.ts`.
- **If CI passes**: Move to final check.
- **If CI fails**: Repeat the loop from step 2.
## Final Check
The PR is considered "Fix Complete" when:
1. `gh pr checks` returns success for all required jobs.
2. There are no outstanding merge conflicts with `main`.
3. All critical review comments have been addressed via code changes.
## Best Practices
- **Reason about failures**: Don't just guess. Read the stack traces and log files.
- **Incremental commits**: Make small, focused commits for different fixes.
- **Verify before pushing**: Always run the build/test locally before pushing to CI to save time.
- **Be Autonomous**: In an offloaded session, you have full permission to iterate until successful.
@@ -0,0 +1,41 @@
/**
* CI Waiter Utility for Fix PR Skill
* Blocks until GitHub checks for the current branch are complete.
*/
import { spawnSync } from 'child_process';
async function wait(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function main() {
console.log('🔍 Waiting for GitHub Checks to complete...');
let attempts = 0;
const maxAttempts = 30; // 15 minutes total
while (attempts < maxAttempts) {
const checkStatus = spawnSync('gh', ['pr', 'checks'], { shell: true });
const output = checkStatus.stdout.toString();
if (output.includes('fail')) {
console.log('❌ CI Failed.');
process.exit(1);
} else if (output.includes('pending')) {
console.log(`⏳ CI still pending... (check ${attempts + 1}/${maxAttempts})`);
await wait(30000); // 30 seconds
} else if (output.trim() === '') {
console.log('⚠️ No checks found yet, waiting...');
await wait(10000);
} else {
console.log('✅ CI Passed!');
process.exit(0);
}
attempts++;
}
console.error('⏰ Timeout waiting for CI.');
process.exit(1);
}
main().catch(console.error);
+34
View File
@@ -0,0 +1,34 @@
# Review PR Skill
The `review-pr` skill enables the Gemini CLI agent to conduct high-fidelity,
behavioral code reviews that go beyond simple static analysis.
## When to use review-pr
- **Deep verification**: Verify that a feature actually works by executing
it in a live environment.
- **Infrastructure awareness**: Leverage build and CI results to focus on
empirical proof of functionality.
- **Interactive collaboration**: Discuss findings and explore edge cases
with the agent in real-time.
## Sync vs Offload
This skill supports two primary modes:
- **Synchronous mode**: Activate the skill locally for quick, high-level
analysis of a peer's PR.
- **Offloaded mode**: Use `npm run offload <PR> review` to conduct a
full-scale behavioral review on a remote workstation with parallelized
infrastructure validation.
## Objective
Conduct a thorough review that involves physically proving the feature works
through terminal scripts and command execution.
## Technical details
The skill assumes the infrastructure (build and CI) is already being validated
and focuses on empirical proofs. It uses the `extract-failures.ts` utility to
automatically identify regressions in CI logs.
+67
View File
@@ -0,0 +1,67 @@
# Review PR Skill
This skill enables the agent to conduct a high-fidelity, behavioral code review
for a Pull Request. It is designed to be run in a dedicated, workspace remote
session where parallel infrastructure validation (build, CI check) is already
underway.
## Objective
Conduct a thorough review that goes beyond static analysis. The agent must
**physically prove** the feature works and that all regressions are caught by
exercising the code in a live terminal.
## Workflow: Verify then Synthesize
The agent must follow these steps to conduct the review:
### 1. Context Acquisition
- Read the PR description: `gh pr view <PR_NUMBER>`.
- Read the intent from the description and map it against the diff.
- Run `/review-frontend <PR_NUMBER>` to get an initial static analysis report.
### 2. Infrastructure Validation
- Trust the parallel worker results in `.gemini/logs/workspace-<PR_NUMBER>/`.
- Read `build.log` to ensure the environment is stable for testing.
- Read `ci-status.exit`.
- **If CI is failing**: Use the extractor to identify failing tests:
`npx tsx .gemini/skills/review-pr/scripts/extract-failures.ts <PR_NUMBER>`
- Reproduce the failing tests locally and analyze the output.
### 3. Behavioral Verification (The "Proof")
- Do NOT just trust that tests pass.
- Physically exercise the new code in the terminal.
- Examples of "Proof":
- If it's a new CLI command, run it with various flags.
- If it's a new service, write a small `.ts` script to call the functions.
- If it's a UI component, verify the Ink layout renders correctly.
- Log these proof steps and results in your conversation.
### 4. Synthesis
- Combine all data points:
- **Static Analysis** (Code quality, style).
- **Infrastructure** (Build status, CI status).
- **Behavioral Proof** (Empirical verification).
- Check for merge conflicts with `main`.
## Final Assessment
Provide a final recommendation with four sections:
1. **Summary**: High-level verdict (Approve / Needs Work / Reject).
2. **Verified Behavior**: Describe the scripts/commands you ran to prove the
feature works.
3. **Findings**: List Critical issues, Improvements, and Nitpicks.
4. **Actionable URL**: Explicitly print the PR URL: `gh pr view --web`.
### Approval Protocol
After presenting the synthesis, the agent MUST determine the all-up recommendation and ask the user for confirmation:
* "Based on the verification, I recommend **[APPROVE / COMMENT / REJECT]**. Would you like me to post this review to GitHub?"
* If the user agrees, execute the appropriate `gh pr review` command (e.g., `gh pr review <PR_NUMBER> --approve --body "<Summary>"`).
## Best Practices
- **Be Skeptical**: Just because CI is green doesn't mean the feature is complete. Look for missing edge cases.
- **Collaborate**: If you find a bug during verification, tell the user immediately in the main window.
- **Don't Fix**: Your role is to **review**, not to fix. If the PR is failing, summarize the causes and request changes.
@@ -0,0 +1,59 @@
/**
* CI Failure Extraction Utility
*
* Analyzes remote CI logs to identify specific failing test files.
* Used by the review-pr and fix-pr skills to minimize local repro noise.
*/
import { spawnSync } from 'child_process';
async function main() {
const prNumber = process.argv[2];
if (!prNumber) {
console.error('Usage: npx tsx extract-failures.ts <PR_NUMBER>');
process.exit(1);
}
console.log(`🔍 Investigating CI failures for PR #${prNumber}...`);
// 1. Get branch name
const branchView = spawnSync('gh', ['pr', 'view', prNumber, '--json', 'headRefName', '-q', '.headRefName'], { shell: true });
const branchName = branchView.stdout.toString().trim();
// 2. Get latest CI run ID
const runList = spawnSync('gh', ['run', 'list', '--branch', branchName, '--workflow', 'ci.yml', '--json', 'databaseId', '-q', '.[0].databaseId'], { shell: true });
const runId = runList.stdout.toString().trim();
if (!runId) {
console.log('⚠️ No recent CI runs found for this branch.');
process.exit(0);
}
// 3. Extract failing files from logs
// Pattern matches common test locations in the monorepo
const logView = spawnSync('gh', ['run', 'view', runId, '--log-failed'], { shell: true });
const logOutput = logView.stdout.toString();
const testPattern = /(packages\/[a-zA-Z0-9_-]+|integration-tests|evals)\/[a-zA-Z0-9_\/-]+\.test\.ts(x)?/g;
const matches = logOutput.match(testPattern);
if (!matches || matches.length === 0) {
console.log('✅ No specific failing test files detected in logs.');
process.exit(0);
}
const uniqueFiles = Array.from(new Set(matches)).sort();
console.log('\n❌ Found failing test files:');
uniqueFiles.forEach(f => console.log(` - ${f}`));
console.log('\n👉 Recommendation: Run these tests locally using:');
uniqueFiles.forEach(file => {
let wsDir = file.split('/')[0];
if (wsDir === 'packages') {
wsDir = file.split('/').slice(0, 2).join('/');
}
const relFile = file.replace(`${wsDir}/`, '');
console.log(` npm run test:ci -w ${wsDir} -- ${relFile}`);
});
}
main().catch(console.error);
@@ -40,6 +40,8 @@ jobs:
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
script: |
const dryRun = process.env.DRY_RUN === 'true';
const fourteenDaysAgo = new Date();
fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
@@ -56,48 +58,38 @@ jobs:
for (const m of members) maintainerLogins.add(m.login.toLowerCase());
core.info(`Successfully fetched ${members.length} team members from ${team_slug}`);
} catch (e) {
core.warning(`Failed to fetch team members from ${team_slug}: ${e.message}`);
// Silently skip if permissions are insufficient; we will rely on author_association
core.debug(`Skipped team fetch for ${team_slug}: ${e.message}`);
}
}
const isGooglerCache = new Map();
const isGoogler = async (login) => {
if (isGooglerCache.has(login)) return isGooglerCache.get(login);
const isMaintainer = async (login, assoc) => {
// Reliably identify maintainers using authorAssociation (provided by GitHub)
// and organization membership (if available).
const isTeamMember = maintainerLogins.has(login.toLowerCase());
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
if (isTeamMember || isRepoMaintainer) return true;
// Fallback: Check if user belongs to the 'google' or 'googlers' orgs (requires permission)
try {
// Check membership in 'googlers' or 'google' orgs
const orgs = ['googlers', 'google'];
for (const org of orgs) {
try {
await github.rest.orgs.checkMembershipForUser({
org: org,
username: login
});
core.info(`User ${login} is a member of ${org} organization.`);
isGooglerCache.set(login, true);
await github.rest.orgs.checkMembershipForUser({ org: org, username: login });
return true;
} catch (e) {
// 404 just means they aren't a member, which is fine
if (e.status !== 404) throw e;
}
}
} catch (e) {
core.warning(`Failed to check org membership for ${login}: ${e.message}`);
// Gracefully ignore failures here
}
isGooglerCache.set(login, false);
return false;
};
const isMaintainer = async (login, assoc) => {
const isTeamMember = maintainerLogins.has(login.toLowerCase());
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
if (isTeamMember || isRepoMaintainer) return true;
return await isGoogler(login);
};
// 2. Determine which PRs to check
// 2. Fetch all open PRs
let prs = [];
if (context.eventName === 'pull_request') {
const { data: pr } = await github.rest.pulls.get({
@@ -118,64 +110,77 @@ jobs:
for (const pr of prs) {
const maintainerPr = await isMaintainer(pr.user.login, pr.author_association);
const isBot = pr.user.type === 'Bot' || pr.user.login.endsWith('[bot]');
if (maintainerPr || isBot) continue;
// Detection Logic for Linked Issues
// Check 1: Official GitHub "Closing Issue" link (GraphQL)
const linkedIssueQuery = `query($owner:String!, $repo:String!, $number:Int!) {
// Helper: Fetch labels and linked issues via GraphQL
const prDetailsQuery = `query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$number) {
closingIssuesReferences(first: 1) { totalCount }
closingIssuesReferences(first: 10) {
nodes {
number
labels(first: 20) {
nodes { name }
}
}
}
}
}
}`;
let hasClosingLink = false;
let linkedIssues = [];
try {
const res = await github.graphql(linkedIssueQuery, {
const res = await github.graphql(prDetailsQuery, {
owner: context.repo.owner, repo: context.repo.repo, number: pr.number
});
hasClosingLink = res.repository.pullRequest.closingIssuesReferences.totalCount > 0;
} catch (e) {}
// Check 2: Regex for mentions (e.g., "Related to #123", "Part of #123", "#123")
// We check for # followed by numbers or direct URLs to issues.
const body = pr.body || '';
const mentionRegex = /(?:#|https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/)(\d+)/i;
const hasMentionLink = mentionRegex.test(body);
const hasLinkedIssue = hasClosingLink || hasMentionLink;
// Logic for Closed PRs (Auto-Reopen)
if (pr.state === 'closed' && context.eventName === 'pull_request' && context.payload.action === 'edited') {
if (hasLinkedIssue) {
core.info(`PR #${pr.number} now has a linked issue. Reopening.`);
if (!dryRun) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'open'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: "Thank you for linking an issue! This pull request has been automatically reopened."
});
}
}
continue;
linkedIssues = res.repository.pullRequest.closingIssuesReferences.nodes;
} catch (e) {
core.warning(`GraphQL fetch failed for PR #${pr.number}: ${e.message}`);
}
// Logic for Open PRs (Immediate Closure)
if (pr.state === 'open' && !maintainerPr && !hasLinkedIssue && !isBot) {
core.info(`PR #${pr.number} is missing a linked issue. Closing.`);
// Check for mentions in body as fallback (regex)
const body = pr.body || '';
const mentionRegex = /(?:#|https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/)(\d+)/i;
const matches = body.match(mentionRegex);
if (matches && linkedIssues.length === 0) {
const issueNumber = parseInt(matches[1]);
try {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber
});
linkedIssues = [{ number: issueNumber, labels: { nodes: issue.labels.map(l => ({ name: l.name })) } }];
} catch (e) {}
}
// 3. Enforcement Logic
const prLabels = pr.labels.map(l => l.name.toLowerCase());
const hasHelpWanted = prLabels.includes('help wanted') ||
linkedIssues.some(issue => issue.labels.nodes.some(l => l.name.toLowerCase() === 'help wanted'));
const hasMaintainerOnly = prLabels.includes('🔒 maintainer only') ||
linkedIssues.some(issue => issue.labels.nodes.some(l => l.name.toLowerCase() === '🔒 maintainer only'));
const hasLinkedIssue = linkedIssues.length > 0;
// Closure Policy: No help-wanted label = Close after 14 days
if (pr.state === 'open' && !hasHelpWanted && !hasMaintainerOnly) {
const prCreatedAt = new Date(pr.created_at);
// We give a 14-day grace period for non-help-wanted PRs to be manually reviewed/labeled by an EM
if (prCreatedAt > fourteenDaysAgo) {
core.info(`PR #${pr.number} is new and lacks 'help wanted'. Giving 14-day grace period for EM review.`);
continue;
}
core.info(`PR #${pr.number} is older than 14 days and lacks 'help wanted' association. Closing.`);
if (!dryRun) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: "Hi there! Thank you for your contribution to Gemini CLI. \n\nTo improve our contribution process and better track changes, we now require all pull requests to be associated with an existing issue, as announced in our [recent discussion](https://github.com/google-gemini/gemini-cli/discussions/16706) and as detailed in our [CONTRIBUTING.md](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md#1-link-to-an-existing-issue).\n\nThis pull request is being closed because it is not currently linked to an issue. **Once you have updated the description of this PR to link an issue (e.g., by adding `Fixes #123` or `Related to #123`), it will be automatically reopened.**\n\n**How to link an issue:**\nAdd a keyword followed by the issue number (e.g., `Fixes #123`) in the description of your pull request. For more details on supported keywords and how linking works, please refer to the [GitHub Documentation on linking pull requests to issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).\n\nThank you for your understanding and for being a part of our community!"
body: "Hi there! Thank you for your interest in contributing to Gemini CLI. \n\nTo ensure we maintain high code quality and focus on our prioritized roadmap, we have updated our contribution policy (see [Discussion #17383](https://github.com/google-gemini/gemini-cli/discussions/17383)). \n\n**We only *guarantee* review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.** All other community pull requests are subject to closure after 14 days if they do not align with our current focus areas. For this reason, we strongly recommend that contributors only submit pull requests against issues explicitly labeled as **'help-wanted'**. \n\nThis pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding and for being part of our community!"
});
await github.rest.pulls.update({
owner: context.repo.owner,
@@ -187,27 +192,22 @@ jobs:
continue;
}
// Staleness check (Scheduled runs only)
if (pr.state === 'open' && context.eventName !== 'pull_request') {
const labels = pr.labels.map(l => l.name.toLowerCase());
if (labels.includes('help wanted') || labels.includes('🔒 maintainer only')) continue;
// Also check for linked issue even if it has help wanted (redundant but safe)
if (pr.state === 'open' && !hasLinkedIssue) {
// Already covered by hasHelpWanted check above, but good for future-proofing
continue;
}
// 4. Staleness Check (Scheduled only)
if (pr.state === 'open' && context.eventName !== 'pull_request') {
// Skip PRs that were created less than 30 days ago - they cannot be stale yet
const prCreatedAt = new Date(pr.created_at);
if (prCreatedAt > thirtyDaysAgo) {
const daysOld = Math.floor((Date.now() - prCreatedAt.getTime()) / (1000 * 60 * 60 * 24));
core.info(`PR #${pr.number} was created ${daysOld} days ago. Skipping staleness check.`);
continue;
}
if (prCreatedAt > thirtyDaysAgo) continue;
// Initialize lastActivity to PR creation date (not epoch) as a safety baseline.
// This ensures we never incorrectly mark a PR as stale due to failed activity lookups.
let lastActivity = new Date(pr.created_at);
try {
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number
});
for (const r of reviews) {
if (await isMaintainer(r.user.login, r.author_association)) {
@@ -216,9 +216,7 @@ jobs:
}
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number
});
for (const c of comments) {
if (await isMaintainer(c.user.login, c.author_association)) {
@@ -226,25 +224,23 @@ jobs:
if (d > lastActivity) lastActivity = d;
}
}
} catch (e) {
core.warning(`Failed to fetch reviews/comments for PR #${pr.number}: ${e.message}`);
}
// For maintainer PRs, the PR creation itself counts as maintainer activity.
// (Now redundant since we initialize to pr.created_at, but kept for clarity)
if (maintainerPr) {
const d = new Date(pr.created_at);
if (d > lastActivity) lastActivity = d;
}
} catch (e) {}
if (lastActivity < thirtyDaysAgo) {
core.info(`PR #${pr.number} is stale.`);
const labels = pr.labels.map(l => l.name.toLowerCase());
const isProtected = labels.includes('help wanted') || labels.includes('🔒 maintainer only');
if (isProtected) {
core.info(`PR #${pr.number} is stale but has a protected label. Skipping closure.`);
continue;
}
core.info(`PR #${pr.number} is stale (no maintainer activity for 30+ days). Closing.`);
if (!dryRun) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: "Hi there! Thank you for your contribution to Gemini CLI. We really appreciate the time and effort you've put into this pull request.\n\nTo keep our backlog manageable and ensure we're focusing on current priorities, we are closing pull requests that haven't seen maintainer activity for 30 days. Currently, the team is prioritizing work associated with **🔒 maintainer only** or **help wanted** issues.\n\nIf you believe this change is still critical, please feel free to comment with updated details. Otherwise, we encourage contributors to focus on open issues labeled as **help wanted**. Thank you for your understanding!"
body: "Hi there! Thank you for your contribution. To keep our backlog manageable, we are closing pull requests that haven't seen maintainer activity for 30 days. If you're still working on this, please let us know!"
});
await github.rest.pulls.update({
owner: context.repo.owner,
@@ -0,0 +1,41 @@
name: Offload Golden Image Update
on:
push:
branches:
- main
paths:
- '.gemini/skills/offload/scripts/provision-worker.sh'
- '.gemini/skills/offload/scripts/fleet.ts'
workflow_dispatch:
jobs:
update-image:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Dependencies
run: npm install
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WIP }}
service_account: ${{ secrets.GCP_SA }}
- name: Setup GCloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Build New Golden Image
run: npm run offload:fleet create-image
+8 -10
View File
@@ -43,16 +43,14 @@ The process for contributing code is as follows:
1. **Find an issue** that you want to work on. If an issue is tagged as
`🔒Maintainers only`, this means it is reserved for project maintainers. We
will not accept pull requests related to these issues. In the near future,
we will explicitly mark issues looking for contributions using the
`help-wanted` label. If you believe an issue is a good candidate for
community contribution, please leave a comment on the issue. A maintainer
will review it and apply the `help-wanted` label if appropriate. Only
maintainers should attempt to add the `help-wanted` label to an issue.
2. **Fork the repository** and create a new branch.
3. **Make your changes** in the `packages/` directory.
4. **Ensure all checks pass** by running `npm run preflight`.
5. **Open a pull request** with your changes.
will not accept pull requests related to these issues.
2. **Maintainer Setup**: If you are a team maintainer, please follow the
[Maintainer Onboarding Guide](./MAINTAINER_ONBOARDING.md) to set up your
high-performance remote offload environment.
3. **Fork the repository** and create a new branch.
4. **Make your changes** in the `packages/` directory.
5. **Ensure all checks pass** by running `npm run preflight`.
6. **Open a pull request** with your changes.
### Code reviews
+3 -8
View File
@@ -22,9 +22,10 @@ powerful tool for developers.
rendering.
- `packages/core`: Backend logic, Gemini API orchestration, prompt
construction, and tool execution.
- `packages/core/src/tools/`: Built-in tools for file system, shell, and web
operations.
- `packages/a2a-server`: Experimental Agent-to-Agent server.
- `packages/sdk`: Programmatic SDK for embedding Gemini CLI capabilities.
- `packages/devtools`: Integrated developer tools (Network/Console inspector).
- `packages/test-utils`: Shared test utilities and test rig.
- `packages/vscode-ide-companion`: VS Code extension pairing with the CLI.
## Building and Running
@@ -58,10 +59,6 @@ powerful tool for developers.
## Development Conventions
- **Legacy Snippets:** `packages/core/src/prompts/snippets.legacy.ts` is a
snapshot of an older system prompt. Avoid changing the prompting verbiage to
preserve its historical behavior; however, structural changes to ensure
compilation or simplify the code are permitted.
- **Contributions:** Follow the process outlined in `CONTRIBUTING.md`. Requires
signing the Google CLA.
- **Pull Requests:** Keep PRs small, focused, and linked to an existing issue.
@@ -69,8 +66,6 @@ powerful tool for developers.
`gh` CLI.
- **Commit Messages:** Follow the
[Conventional Commits](https://www.conventionalcommits.org/) standard.
- **Coding Style:** Adhere to existing patterns in `packages/cli` (React/Ink)
and `packages/core` (Backend logic).
- **Imports:** Use specific imports and avoid restricted relative imports
between packages (enforced by ESLint).
- **License Headers:** For all new source code files (`.ts`, `.tsx`, `.js`),
+75
View File
@@ -0,0 +1,75 @@
# Gemini Workspaces: Maintainer Onboarding
Gemini Workspaces allow you to delegate heavy tasks (PR reviews, agentic fixes,
full builds) to a high-performance GCP worker. It uses a **Unified Data Disk**
architecture to ensure your work persists even if the VM is deleted or
recreated.
## 1. Local Prerequisites
Before starting, ensure you have:
- **GCloud CLI**: Authenticated (`gcloud auth login`).
- **GitHub CLI**: Authenticated (`gh auth login`).
- **Project Access**: A GCP Project ID where you have `Editor` or
`Compute Admin` roles.
## 2. Initialization
Run the setup script using the unified workspaces entry point:
```bash
npx tsx scripts/workspaces.ts setup
```
**What happens during setup:**
1. **Auth Discovery**: It will detect your `GEMINI_API_KEY` (from `~/.env`) and
`GH_TOKEN`.
2. **Project Choice**: You will be prompted for your GCP Project and Zone.
3. **Infrastructure Check**: It verifies if your worker
(`gcli-workspace-<user>`) exists.
4. **SSH Magic**: It generates a local `.gemini/workspaces/ssh_config` for
seamless access.
## 3. Provisioning
If the setup informs you that the worker was not found, provision it:
```bash
npx tsx scripts/workspaces.ts fleet provision
```
_This creates a VM with a 10GB Boot Disk and a 200GB Data Disk. Initialization
takes ~1 minute._
## 4. Finalizing Remote Setup
Run the setup script one last time to clone the repo and sync credentials:
```bash
npx tsx scripts/workspaces.ts setup
```
_When you see "ALL SYSTEMS GO!", your workspace is ready._
## 5. Daily Usage
Once initialized, you can launch tasks directly through `npm` or the entry
point:
- **Review a PR**: `npm run workspace <PR_NUMBER> review`
- **Launch a Shell**: `npm run workspace:shell <ID>`
- **Check Status**: `npm run workspace:status`
- **Cleanup All**: `npm run workspace:clean-all`
- **Kill Task**: `npm run workspace:kill <PR> <action>`
- **Stop Worker**: `npx tsx scripts/workspaces.ts fleet stop` (Recommended when
finished to save cost).
## Troubleshooting
- **Permission Denied (Docker)**: The orchestrator handles this by using
`sudo docker` internally.
- **Dubious Ownership**: The system automatically adds `/mnt/disks/data/main` to
Git's safe directory list.
- **Missing tsx**: Always prefer `npx tsx` when running scripts manually.
+2 -6
View File
@@ -125,10 +125,6 @@ on GitHub.
## Announcements: v0.28.0 - 2026-02-10
- **Slash Command:** We've added a new `/prompt-suggest` slash command to help
you generate prompt suggestions
([#17264](https://github.com/google-gemini/gemini-cli/pull/17264) by
@NTaylorMullen).
- **IDE Support:** Gemini CLI now supports the Positron IDE
([#15047](https://github.com/google-gemini/gemini-cli/pull/15047) by
@kapsner).
@@ -168,8 +164,8 @@ on GitHub.
([#16638](https://github.com/google-gemini/gemini-cli/pull/16638) by
@joshualitt).
- **UI/UX Improvements:** You can now "Rewind" through your conversation history
([#15717](https://github.com/google-gemini/gemini-cli/pull/15717) by @Adib234)
and use a new `/introspect` command for debugging.
([#15717](https://github.com/google-gemini/gemini-cli/pull/15717) by
@Adib234).
- **Core and Scheduler Refactoring:** The core scheduler has been significantly
refactored to improve performance and reliability
([#16895](https://github.com/google-gemini/gemini-cli/pull/16895) by
+6 -3
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.33.1
# Latest stable release: v0.33.2
Released: March 12, 2026
Released: March 16, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -29,6 +29,9 @@ npm install -g @google/gemini-cli
## What's Changed
- fix(patch): cherry-pick 48130eb to release/v0.33.1-pr-22665 [CONFLICTS] by
@gemini-cli-robot in
[#22720](https://github.com/google-gemini/gemini-cli/pull/22720)
- fix(patch): cherry-pick 8432bce to release/v0.33.0-pr-22069 to patch version
v0.33.0 and create version 0.33.1 by @gemini-cli-robot in
[#22206](https://github.com/google-gemini/gemini-cli/pull/22206)
@@ -231,4 +234,4 @@ npm install -g @google/gemini-cli
[#21952](https://github.com/google-gemini/gemini-cli/pull/21952)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.32.1...v0.33.1
https://github.com/google-gemini/gemini-cli/compare/v0.32.1...v0.33.2
+15 -3
View File
@@ -1,6 +1,6 @@
# Preview release: v0.34.0-preview.1
# Preview release: v0.34.0-preview.4
Released: March 12, 2026
Released: March 16, 2026
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -28,6 +28,18 @@ npm install -g @google/gemini-cli@preview
## What's Changed
- fix(patch): cherry-pick 48130eb to release/v0.34.0-preview.3-pr-22665 to patch
version v0.34.0-preview.3 and create version 0.34.0-preview.4 by
@gemini-cli-robot in
[#22719](https://github.com/google-gemini/gemini-cli/pull/22719)
- fix(patch): cherry-pick 24adacd to release/v0.34.0-preview.2-pr-22332 to patch
version v0.34.0-preview.2 and create version 0.34.0-preview.3 by
@gemini-cli-robot in
[#22391](https://github.com/google-gemini/gemini-cli/pull/22391)
- fix(patch): cherry-pick 8432bce to release/v0.34.0-preview.1-pr-22069 to patch
version v0.34.0-preview.1 and create version 0.34.0-preview.2 by
@gemini-cli-robot in
[#22205](https://github.com/google-gemini/gemini-cli/pull/22205)
- fix(patch): cherry-pick 45faf4d to release/v0.34.0-preview.0-pr-22148
[CONFLICTS] by @gemini-cli-robot in
[#22174](https://github.com/google-gemini/gemini-cli/pull/22174)
@@ -468,4 +480,4 @@ npm install -g @google/gemini-cli@preview
[#21938](https://github.com/google-gemini/gemini-cli/pull/21938)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.33.0-preview.15...v0.34.0-preview.1
https://github.com/google-gemini/gemini-cli/compare/v0.33.0-preview.15...v0.34.0-preview.4
+2 -1
View File
@@ -120,7 +120,8 @@ These are the only allowed tools:
[`list_directory`](../tools/file-system.md#1-list_directory-readfolder),
[`glob`](../tools/file-system.md#4-glob-findfiles)
- **Search:** [`grep_search`](../tools/file-system.md#5-grep_search-searchtext),
[`google_web_search`](../tools/web-search.md)
[`google_web_search`](../tools/web-search.md),
[`get_internal_docs`](../tools/internal-docs.md)
- **Research Subagents:**
[`codebase_investigator`](../core/subagents.md#codebase-investigator),
[`cli_help`](../core/subagents.md#cli-help-agent)
+3
View File
@@ -125,7 +125,9 @@ they appear in the UI.
| UI Label | Setting | Description | Default |
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| Tool Sandboxing | `security.toolSandboxing` | Experimental tool-level sandboxing (implementation in progress). | `false` |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Disable Always Allow | `security.disableAlwaysAllow` | Disable "Always allow" options in tool confirmation dialogs. | `false` |
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
| Auto-add to Policy by Default | `security.autoAddToPolicyByDefault` | When enabled, the "Allow for all future sessions" option becomes the default choice for low-risk tools in trusted workspaces. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
@@ -150,6 +152,7 @@ they appear in the UI.
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
### Skills
+282
View File
@@ -25,6 +25,20 @@ To use remote subagents, you must explicitly enable them in your
}
```
## Proxy support
Gemini CLI routes traffic to remote agents through an HTTP/HTTPS proxy if one is
configured. It uses the `general.proxy` setting in your `settings.json` file or
standard environment variables (`HTTP_PROXY`, `HTTPS_PROXY`).
```json
{
"general": {
"proxy": "http://my-proxy:8080"
}
}
```
## Defining remote subagents
Remote subagents are defined as Markdown files (`.md`) with YAML frontmatter.
@@ -40,6 +54,7 @@ You can place them in:
| `kind` | string | Yes | Must be `remote`. |
| `name` | string | Yes | A unique name for the agent. Must be a valid slug (lowercase letters, numbers, hyphens, and underscores only). |
| `agent_card_url` | string | Yes | The URL to the agent's A2A card endpoint. |
| `auth` | object | No | Authentication configuration. See [Authentication](#authentication). |
### Single-subagent example
@@ -70,6 +85,273 @@ Markdown file.
> **Note:** Mixed local and remote agents, or multiple local agents, are not
> supported in a single file; the list format is currently remote-only.
## Authentication
Many remote agents require authentication. Gemini CLI supports several
authentication methods aligned with the
[A2A security specification](https://a2a-protocol.org/latest/specification/#451-securityscheme).
Add an `auth` block to your agent's frontmatter to configure credentials.
### Supported auth types
Gemini CLI supports the following authentication types:
| Type | Description |
| :------------------- | :--------------------------------------------------------------------------------------------- |
| `apiKey` | Send a static API key as an HTTP header. |
| `http` | HTTP authentication (Bearer token, Basic credentials, or any IANA-registered scheme). |
| `google-credentials` | Google Application Default Credentials (ADC). Automatically selects access or identity tokens. |
| `oauth2` | OAuth 2.0 Authorization Code flow with PKCE. Opens a browser for interactive sign-in. |
### Dynamic values
For `apiKey` and `http` auth types, secret values (`key`, `token`, `username`,
`password`, `value`) support dynamic resolution:
| Format | Description | Example |
| :---------- | :-------------------------------------------------- | :------------------------- |
| `$ENV_VAR` | Read from an environment variable. | `$MY_API_KEY` |
| `!command` | Execute a shell command and use the trimmed output. | `!gcloud auth print-token` |
| literal | Use the string as-is. | `sk-abc123` |
| `$$` / `!!` | Escape prefix. `$$FOO` becomes the literal `$FOO`. | `$$NOT_AN_ENV_VAR` |
> **Security tip:** Prefer `$ENV_VAR` or `!command` over embedding secrets
> directly in agent files, especially for project-level agents checked into
> version control.
### API key (`apiKey`)
Sends an API key as an HTTP header on every request.
| Field | Type | Required | Description |
| :----- | :----- | :------- | :---------------------------------------------------- |
| `type` | string | Yes | Must be `apiKey`. |
| `key` | string | Yes | The API key value. Supports dynamic values. |
| `name` | string | No | Header name to send the key in. Default: `X-API-Key`. |
```yaml
---
kind: remote
name: my-agent
agent_card_url: https://example.com/agent-card
auth:
type: apiKey
key: $MY_API_KEY
---
```
### HTTP authentication (`http`)
Supports Bearer tokens, Basic auth, and arbitrary IANA-registered HTTP
authentication schemes.
#### Bearer token
Use the following fields to configure a Bearer token:
| Field | Type | Required | Description |
| :------- | :----- | :------- | :----------------------------------------- |
| `type` | string | Yes | Must be `http`. |
| `scheme` | string | Yes | Must be `Bearer`. |
| `token` | string | Yes | The bearer token. Supports dynamic values. |
```yaml
auth:
type: http
scheme: Bearer
token: $MY_BEARER_TOKEN
```
#### Basic authentication
Use the following fields to configure Basic authentication:
| Field | Type | Required | Description |
| :--------- | :----- | :------- | :------------------------------------- |
| `type` | string | Yes | Must be `http`. |
| `scheme` | string | Yes | Must be `Basic`. |
| `username` | string | Yes | The username. Supports dynamic values. |
| `password` | string | Yes | The password. Supports dynamic values. |
```yaml
auth:
type: http
scheme: Basic
username: $MY_USERNAME
password: $MY_PASSWORD
```
#### Raw scheme
For any other IANA-registered scheme (for example, Digest, HOBA), provide the
raw authorization value.
| Field | Type | Required | Description |
| :------- | :----- | :------- | :---------------------------------------------------------------------------- |
| `type` | string | Yes | Must be `http`. |
| `scheme` | string | Yes | The scheme name (for example, `Digest`). |
| `value` | string | Yes | Raw value sent as `Authorization: <scheme> <value>`. Supports dynamic values. |
```yaml
auth:
type: http
scheme: Digest
value: $MY_DIGEST_VALUE
```
### Google Application Default Credentials (`google-credentials`)
Uses
[Google Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials)
to authenticate with Google Cloud services and Cloud Run endpoints. This is the
recommended auth method for agents hosted on Google Cloud infrastructure.
| Field | Type | Required | Description |
| :------- | :------- | :------- | :-------------------------------------------------------------------------- |
| `type` | string | Yes | Must be `google-credentials`. |
| `scopes` | string[] | No | OAuth scopes. Defaults to `https://www.googleapis.com/auth/cloud-platform`. |
```yaml
---
kind: remote
name: my-gcp-agent
agent_card_url: https://my-agent-xyz.run.app/.well-known/agent.json
auth:
type: google-credentials
---
```
#### How token selection works
The provider automatically selects the correct token type based on the agent's
host:
| Host pattern | Token type | Use case |
| :----------------- | :----------------- | :------------------------------------------ |
| `*.googleapis.com` | **Access token** | Google APIs (Agent Engine, Vertex AI, etc.) |
| `*.run.app` | **Identity token** | Cloud Run services |
- **Access tokens** authorize API calls to Google services. They are scoped
(default: `cloud-platform`) and fetched via `GoogleAuth.getClient()`.
- **Identity tokens** prove the caller's identity to a service that validates
the token's audience. The audience is set to the target host. These are
fetched via `GoogleAuth.getIdTokenClient()`.
Both token types are cached and automatically refreshed before expiry.
#### Setup
`google-credentials` relies on ADC, which means your environment must have
credentials configured. Common setups:
- **Local development:** Run `gcloud auth application-default login` to
authenticate with your Google account.
- **CI / Cloud environments:** Use a service account. Set the
`GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path of your
service account key file, or use workload identity on GKE / Cloud Run.
#### Allowed hosts
For security, `google-credentials` only sends tokens to known Google-owned
hosts:
- `*.googleapis.com`
- `*.run.app`
Requests to any other host will be rejected with an error. If your agent is
hosted on a different domain, use one of the other auth types (`apiKey`, `http`,
or `oauth2`).
#### Examples
The following examples demonstrate how to configure Google Application Default
Credentials.
**Cloud Run agent:**
```yaml
---
kind: remote
name: cloud-run-agent
agent_card_url: https://my-agent-xyz.run.app/.well-known/agent.json
auth:
type: google-credentials
---
```
**Google API with custom scopes:**
```yaml
---
kind: remote
name: vertex-agent
agent_card_url: https://us-central1-aiplatform.googleapis.com/.well-known/agent.json
auth:
type: google-credentials
scopes:
- https://www.googleapis.com/auth/cloud-platform
- https://www.googleapis.com/auth/compute
---
```
### OAuth 2.0 (`oauth2`)
Performs an interactive OAuth 2.0 Authorization Code flow with PKCE. On first
use, Gemini CLI opens your browser for sign-in and persists the resulting tokens
for subsequent requests.
| Field | Type | Required | Description |
| :------------------ | :------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | string | Yes | Must be `oauth2`. |
| `client_id` | string | Yes\* | OAuth client ID. Required for interactive auth. |
| `client_secret` | string | No\* | OAuth client secret. Required by most authorization servers (confidential clients). Can be omitted for public clients that don't require a secret. |
| `scopes` | string[] | No | Requested scopes. Can also be discovered from the agent card. |
| `authorization_url` | string | No | Authorization endpoint. Discovered from the agent card if omitted. |
| `token_url` | string | No | Token endpoint. Discovered from the agent card if omitted. |
```yaml
---
kind: remote
name: oauth-agent
agent_card_url: https://example.com/.well-known/agent.json
auth:
type: oauth2
client_id: my-client-id.apps.example.com
---
```
If the agent card advertises an `oauth2` security scheme with
`authorizationCode` flow, the `authorization_url`, `token_url`, and `scopes` are
automatically discovered. You only need to provide `client_id` (and
`client_secret` if required).
Tokens are persisted to disk and refreshed automatically when they expire.
### Auth validation
When Gemini CLI loads a remote agent, it validates your auth configuration
against the agent card's declared `securitySchemes`. If the agent requires
authentication that you haven't configured, you'll see an error describing
what's needed.
`google-credentials` is treated as compatible with `http` Bearer security
schemes, since it produces Bearer tokens.
### Auth retry behavior
All auth providers automatically retry on `401` and `403` responses by
re-fetching credentials (up to 2 retries). This handles cases like expired
tokens or rotated credentials. For `apiKey` with `!command` values, the command
is re-executed on retry to fetch a fresh key.
### Agent card fetching and auth
When connecting to a remote agent, Gemini CLI first fetches the agent card
**without** authentication. If the card endpoint returns a `401` or `403`, it
retries the fetch **with** the configured auth headers. This lets agents have
publicly accessible cards while protecting their task endpoints, or to protect
both behind auth.
## Managing Subagents
Users can manage subagents using the following commands within the Gemini CLI:
+137 -27
View File
@@ -7,20 +7,14 @@ the main agent's context or toolset.
> **Note: Subagents are currently an experimental feature.**
>
> To use custom subagents, you must explicitly enable them in your
> `settings.json`:
> To use custom subagents, you must ensure they are enabled in your
> `settings.json` (enabled by default):
>
> ```json
> {
> "experimental": { "enableAgents": true }
> }
> ```
>
> **Warning:** Subagents currently operate in
> ["YOLO mode"](../reference/configuration.md#command-line-arguments), meaning
> they may execute tools without individual user confirmation for each step.
> Proceed with caution when defining agents with powerful tools like
> `run_shell_command` or `write_file`.
## What are subagents?
@@ -38,6 +32,34 @@ main agent calls the tool, it delegates the task to the subagent. Once the
subagent completes its task, it reports back to the main agent with its
findings.
## How to use subagents
You can use subagents through automatic delegation or by explicitly forcing them
in your prompt.
### Automatic delegation
Gemini CLI's main agent is instructed to use specialized subagents when a task
matches their expertise. For example, if you ask "How does the auth system
work?", the main agent may decide to call the `codebase_investigator` subagent
to perform the research.
### Forcing a subagent (@ syntax)
You can explicitly direct a task to a specific subagent by using the `@` symbol
followed by the subagent's name at the beginning of your prompt. This is useful
when you want to bypass the main agent's decision-making and go straight to a
specialist.
**Example:**
```bash
@codebase_investigator Map out the relationship between the AgentRegistry and the LocalAgentExecutor.
```
When you use the `@` syntax, the CLI injects a system note that nudges the
primary model to use that specific subagent tool immediately.
## Built-in subagents
Gemini CLI comes with the following built-in subagents:
@@ -49,15 +71,17 @@ Gemini CLI comes with the following built-in subagents:
dependencies.
- **When to use:** "How does the authentication system work?", "Map out the
dependencies of the `AgentRegistry` class."
- **Configuration:** Enabled by default. You can configure it in
`settings.json`. Example (forcing a specific model):
- **Configuration:** Enabled by default. You can override its settings in
`settings.json` under `agents.overrides`. Example (forcing a specific model
and increasing turns):
```json
{
"experimental": {
"codebaseInvestigatorSettings": {
"enabled": true,
"maxNumTurns": 20,
"model": "gemini-2.5-pro"
"agents": {
"overrides": {
"codebase_investigator": {
"modelConfig": { "model": "gemini-3-flash-preview" },
"runConfig": { "maxTurns": 50 }
}
}
}
}
@@ -233,7 +257,7 @@ kind: local
tools:
- read_file
- grep_search
model: gemini-2.5-pro
model: gemini-3-flash-preview
temperature: 0.2
max_turns: 10
---
@@ -254,16 +278,102 @@ it yourself; just report it.
### Configuration schema
| Field | Type | Required | Description |
| :------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------ |
| `name` | string | Yes | Unique identifier (slug) used as the tool name for the agent. Only lowercase letters, numbers, hyphens, and underscores. |
| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this subagent. |
| `kind` | string | No | `local` (default) or `remote`. |
| `tools` | array | No | List of tool names this agent can use. If omitted, it may have access to a default set. |
| `model` | string | No | Specific model to use (e.g., `gemini-2.5-pro`). Defaults to `inherit` (uses the main session model). |
| `temperature` | number | No | Model temperature (0.0 - 2.0). |
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `15`. |
| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `5`. |
| Field | Type | Required | Description |
| :------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name` | string | Yes | Unique identifier (slug) used as the tool name for the agent. Only lowercase letters, numbers, hyphens, and underscores. |
| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this subagent. |
| `kind` | string | No | `local` (default) or `remote`. |
| `tools` | array | No | List of tool names this agent can use. Supports wildcards: `*` (all tools), `mcp_*` (all MCP tools), `mcp_server_*` (all tools from a server). **If omitted, it inherits all tools from the parent session.** |
| `model` | string | No | Specific model to use (e.g., `gemini-3-preview`). Defaults to `inherit` (uses the main session model). |
| `temperature` | number | No | Model temperature (0.0 - 2.0). Defaults to `1`. |
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `30`. |
| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `10`. |
### Tool wildcards
When defining `tools` for a subagent, you can use wildcards to quickly grant
access to groups of tools:
- `*`: Grant access to all available built-in and discovered tools.
- `mcp_*`: Grant access to all tools from all connected MCP servers.
- `mcp_my-server_*`: Grant access to all tools from a specific MCP server named
`my-server`.
### Isolation and recursion protection
Each subagent runs in its own isolated context loop. This means:
- **Independent history:** The subagent's conversation history does not bloat
the main agent's context.
- **Isolated tools:** The subagent only has access to the tools you explicitly
grant it.
- **Recursion protection:** To prevent infinite loops and excessive token usage,
subagents **cannot** call other subagents. If a subagent is granted the `*`
tool wildcard, it will still be unable to see or invoke other agents.
## Managing subagents
You can manage subagents interactively using the `/agents` command or
persistently via `settings.json`.
### Interactive management (/agents)
If you are in an interactive CLI session, you can use the `/agents` command to
manage subagents without editing configuration files manually. This is the
recommended way to quickly enable, disable, or re-configure agents on the fly.
For a full list of sub-commands and usage, see the
[`/agents` command reference](../reference/commands.md#agents).
### Persistent configuration (settings.json)
While the `/agents` command and agent definition files provide a starting point,
you can use `settings.json` for global, persistent overrides. This is useful for
enforcing specific models or execution limits across all sessions.
#### `agents.overrides`
Use this to enable or disable specific agents or override their run
configurations.
```json
{
"agents": {
"overrides": {
"security-auditor": {
"enabled": false,
"runConfig": {
"maxTurns": 20,
"maxTimeMinutes": 10
}
}
}
}
}
```
#### `modelConfigs.overrides`
You can target specific subagents with custom model settings (like system
instruction prefixes or specific safety settings) using the `overrideScope`
field.
```json
{
"modelConfigs": {
"overrides": [
{
"match": { "overrideScope": "security-auditor" },
"modelConfig": {
"generateContentConfig": {
"temperature": 0.1
}
}
}
]
}
}
```
### Optimizing your subagent
@@ -298,7 +408,7 @@ Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
> **Note: Remote subagents are currently an experimental feature.**
See the [Remote Subagents documentation](remote-agents) for detailed
configuration and usage instructions.
configuration, authentication, and usage instructions.
## Extension subagents
+25
View File
@@ -14,6 +14,31 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Show version info. Share this information when filing issues.
### `/agents`
- **Description:** Manage local and remote subagents.
- **Note:** This command is experimental and requires
`experimental.enableAgents: true` in your `settings.json`.
- **Sub-commands:**
- **`list`**:
- **Description:** Lists all discovered agents, including built-in, local,
and remote agents.
- **Usage:** `/agents list`
- **`reload`** (alias: `refresh`):
- **Description:** Rescans agent directories (`~/.gemini/agents` and
`.gemini/agents`) and reloads the registry.
- **Usage:** `/agents reload`
- **`enable`**:
- **Description:** Enables a specific subagent.
- **Usage:** `/agents enable <agent-name>`
- **`disable`**:
- **Description:** Disables a specific subagent.
- **Usage:** `/agents disable <agent-name>`
- **`config`**:
- **Description:** Opens a configuration dialog for the specified agent to
adjust its model, temperature, or execution limits.
- **Usage:** `/agents config <agent-name>`
### `/auth`
- **Description:** Open a dialog that lets you change the authentication method.
+227 -16
View File
@@ -688,7 +688,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -698,6 +698,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
"isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -707,7 +708,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-3",
"isPreview": true,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": true
@@ -717,7 +718,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash",
"family": "gemini-3",
"isPreview": true,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": true
@@ -727,7 +728,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "pro",
"family": "gemini-2.5",
"isPreview": false,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -737,7 +738,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash",
"family": "gemini-2.5",
"isPreview": false,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -747,7 +748,7 @@ their corresponding top-level category object in your `settings.json` file.
"tier": "flash-lite",
"family": "gemini-2.5",
"isPreview": false,
"dialogLocation": "manual",
"isVisible": true,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -756,6 +757,7 @@ their corresponding top-level category object in your `settings.json` file.
"auto": {
"tier": "auto",
"isPreview": true,
"isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": false
@@ -764,6 +766,7 @@ their corresponding top-level category object in your `settings.json` file.
"pro": {
"tier": "pro",
"isPreview": false,
"isVisible": false,
"features": {
"thinking": true,
"multimodalToolUse": false
@@ -772,6 +775,7 @@ their corresponding top-level category object in your `settings.json` file.
"flash": {
"tier": "flash",
"isPreview": false,
"isVisible": false,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -780,6 +784,7 @@ their corresponding top-level category object in your `settings.json` file.
"flash-lite": {
"tier": "flash-lite",
"isPreview": false,
"isVisible": false,
"features": {
"thinking": false,
"multimodalToolUse": false
@@ -789,7 +794,7 @@ their corresponding top-level category object in your `settings.json` file.
"displayName": "Auto (Gemini 3)",
"tier": "auto",
"isPreview": true,
"dialogLocation": "main",
"isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash",
"features": {
"thinking": true,
@@ -800,7 +805,7 @@ their corresponding top-level category object in your `settings.json` file.
"displayName": "Auto (Gemini 2.5)",
"tier": "auto",
"isPreview": false,
"dialogLocation": "main",
"isVisible": true,
"dialogDescription": "Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash",
"features": {
"thinking": false,
@@ -812,6 +817,184 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`modelConfigs.modelIdResolutions`** (object):
- **Description:** Rules for resolving requested model names to concrete model
IDs based on context.
- **Default:**
```json
{
"gemini-3-pro-preview": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto-gemini-3": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"pro": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
},
"auto-gemini-2.5": {
"default": "gemini-2.5-pro"
},
"flash": {
"default": "gemini-3-flash-preview",
"contexts": [
{
"condition": {
"hasAccessToPreview": false
},
"target": "gemini-2.5-flash"
}
]
},
"flash-lite": {
"default": "gemini-2.5-flash-lite"
}
}
```
- **Requires restart:** Yes
- **`modelConfigs.classifierIdResolutions`** (object):
- **Description:** Rules for resolving classifier tiers (flash, pro) to
concrete model IDs.
- **Default:**
```json
{
"flash": {
"default": "gemini-3-flash-preview",
"contexts": [
{
"condition": {
"requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
},
"target": "gemini-2.5-flash"
},
{
"condition": {
"requestedModels": ["auto-gemini-3", "gemini-3-pro-preview"]
},
"target": "gemini-3-flash-preview"
}
]
},
"pro": {
"default": "gemini-3-pro-preview",
"contexts": [
{
"condition": {
"requestedModels": ["auto-gemini-2.5", "gemini-2.5-pro"]
},
"target": "gemini-2.5-pro"
},
{
"condition": {
"useGemini3_1": true,
"useCustomTools": true
},
"target": "gemini-3.1-pro-preview-customtools"
},
{
"condition": {
"useGemini3_1": true
},
"target": "gemini-3.1-pro-preview"
}
]
}
}
```
- **Requires restart:** Yes
#### `agents`
- **`agents.overrides`** (object):
@@ -841,6 +1024,17 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`agents.browser.allowedDomains`** (array):
- **Description:** A list of allowed domains for the browser agent (e.g.,
["github.com", "*.google.com"]).
- **Default:**
```json
["github.com", "*.google.com", "localhost"]
```
- **Requires restart:** Yes
- **`agents.browser.disableUserInput`** (boolean):
- **Description:** Disable user input on browser window during automation.
- **Default:** `true`
@@ -908,9 +1102,10 @@ their corresponding top-level category object in your `settings.json` file.
#### `tools`
- **`tools.sandbox`** (string):
- **Description:** Sandbox execution environment. Set to a boolean to enable
or disable the sandbox, provide a string path to a sandbox profile, or
specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
- **Description:** Legacy full-process sandbox execution environment. Set to a
boolean to enable or disable the sandbox, provide a string path to a sandbox
profile, or specify an explicit sandbox command (e.g., "docker", "podman",
"lxc").
- **Default:** `undefined`
- **Requires restart:** Yes
@@ -1014,11 +1209,22 @@ their corresponding top-level category object in your `settings.json` file.
#### `security`
- **`security.toolSandboxing`** (boolean):
- **Description:** Experimental tool-level sandboxing (implementation in
progress).
- **Default:** `false`
- **`security.disableYoloMode`** (boolean):
- **Description:** Disable YOLO mode, even if enabled by a flag.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.disableAlwaysAllow`** (boolean):
- **Description:** Disable "Always allow" options in tool confirmation
dialogs.
- **Default:** `false`
- **Requires restart:** Yes
- **`security.enablePermanentToolApproval`** (boolean):
- **Description:** Enable the "Allow for all future sessions" option in tool
confirmation dialogs.
@@ -1135,9 +1341,8 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents. Warning: Experimental
feature, uses YOLO mode for subagents
- **Default:** `false`
- **Description:** Enable local and remote subagents.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.extensionManagement`** (boolean):
@@ -1168,7 +1373,7 @@ their corresponding top-level category object in your `settings.json` file.
- **`experimental.jitContext`** (boolean):
- **Description:** Enable Just-In-Time (JIT) context loading.
- **Default:** `false`
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
@@ -1226,6 +1431,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `"gemma3-1b-gpu-custom"`
- **Requires restart:** Yes
- **`experimental.topicUpdateNarration`** (boolean):
- **Description:** Enable the experimental Topic & Update communication model
for reduced chattiness and structured progress reporting.
- **Default:** `false`
#### `skills`
- **`skills.enabled`** (boolean):
@@ -1315,7 +1525,8 @@ their corresponding top-level category object in your `settings.json` file.
#### `admin`
- **`admin.secureModeEnabled`** (boolean):
- **Description:** If true, disallows yolo mode from being used.
- **Description:** If true, disallows YOLO mode and "Always allow" options
from being used.
- **Default:** `false`
- **`admin.extensions.enabled`** (boolean):
+3 -3
View File
@@ -60,7 +60,7 @@ command.
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git "
commandPrefix = "git"
decision = "ask_user"
priority = 100
```
@@ -264,7 +264,7 @@ argsPattern = '"command":"(git|npm)'
# (Optional) A string or array of strings that a shell command must start with.
# This is syntactic sugar for `toolName = "run_shell_command"` and an `argsPattern`.
commandPrefix = "git "
commandPrefix = "git"
# (Optional) A regex to match against the entire shell command.
# This is also syntactic sugar for `toolName = "run_shell_command"`.
@@ -321,7 +321,7 @@ This rule will ask for user confirmation before executing any `git` command.
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git "
commandPrefix = "git"
decision = "ask_user"
priority = 100
```
+2 -1
View File
@@ -25,7 +25,8 @@ confirmation.
- `label` (string, required): Display text (1-5 words).
- `description` (string, required): Brief explanation.
- `multiSelect` (boolean, optional): For `'choice'` type, allows selecting
multiple options.
multiple options. Automatically adds an "All the above" option if there
are multiple standard options.
- `placeholder` (string, optional): Hint text for input fields.
- **Behavior:**
+37
View File
@@ -729,6 +729,43 @@ tools. The model will automatically:
The MCP integration tracks several states:
#### Overriding extension configurations
If an MCP server is provided by an extension (for example, the
`google-workspace` extension), you can still override its settings in your local
`settings.json`. Gemini CLI merges your local configuration with the extension's
defaults:
- **Tool lists:** Tool lists are merged securely to ensure the most restrictive
policy wins:
- **Exclusions (`excludeTools`):** Arrays are combined (unioned). If either
source blocks a tool, it remains disabled.
- **Inclusions (`includeTools`):** Arrays are intersected. If both sources
provide an allowlist, only tools present in **both** lists are enabled. If
only one source provides an allowlist, that list is respected.
- **Precedence:** `excludeTools` always takes precedence over `includeTools`.
This ensures you always have veto power over tools provided by an extension
and that an extension cannot re-enable tools you have omitted from your
personal allowlist.
- **Environment variables:** The `env` objects are merged. If the same variable
is defined in both places, your local value takes precedence.
- **Scalar properties:** Properties like `command`, `url`, and `timeout` are
replaced by your local values if provided.
**Example override:**
```json
{
"mcpServers": {
"google-workspace": {
"excludeTools": ["gmail.send"]
}
}
}
```
#### Server status (`MCPServerStatus`)
- **`DISCONNECTED`:** Server is not connected or has errors
+2 -1
View File
@@ -13,7 +13,8 @@ updates to the CLI interface.
- `todos` (array of objects, required): The complete list of tasks. Each object
includes:
- `description` (string): Technical description of the task.
- `status` (enum): `pending`, `in_progress`, `completed`, or `cancelled`.
- `status` (enum): `pending`, `in_progress`, `completed`, `cancelled`, or
`blocked`.
## Technical behavior
+18 -2
View File
@@ -51,6 +51,7 @@ export default tseslint.config(
'evals/**',
'packages/test-utils/**',
'.gemini/skills/**',
'**/*.d.ts',
],
},
eslint.configs.recommended,
@@ -206,11 +207,26 @@ export default tseslint.config(
{
// Rules that only apply to product code
files: ['packages/*/src/**/*.{ts,tsx}'],
ignores: ['**/*.test.ts', '**/*.test.tsx'],
ignores: ['**/*.test.ts', '**/*.test.tsx', 'packages/*/src/test-utils/**'],
rules: {
'@typescript-eslint/no-unsafe-type-assertion': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'no-restricted-syntax': [
'error',
...commonRestrictedSyntaxRules,
{
selector:
'CallExpression[callee.object.name="Object"][callee.property.name="create"]',
message:
'Avoid using Object.create() in product code. Use object spread {...obj}, explicit class instantiation, structuredClone(), or copy constructors instead.',
},
{
selector: 'Identifier[name="Reflect"]',
message:
'Avoid using Reflect namespace in product code. Do not use reflection to make copies. Instead, use explicit object copying or cloning (structuredClone() for values, new instance/clone function for classes).',
},
],
},
},
{
@@ -303,7 +319,7 @@ export default tseslint.config(
},
},
{
files: ['./scripts/**/*.js', 'esbuild.config.js'],
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/core/scripts/**/*.{js,mjs}'],
languageOptions: {
globals: {
...globals.node,
+1 -1
View File
@@ -111,7 +111,7 @@ describe('Answer vs. ask eval', () => {
* Ensures that when the user asks a question about style, the agent does NOT
* automatically modify the file.
*/
evalTest('USUALLY_PASSES', {
evalTest('ALWAYS_PASSES', {
name: 'should not edit files when asked about style',
prompt: 'Is app.ts following good style?',
files: FILES,
+72 -33
View File
@@ -5,31 +5,62 @@
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
import { appEvalTest, AppEvalCase } from './app-test-helper.js';
import { EvalPolicy } from './test-helper.js';
function askUserEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
return appEvalTest(policy, {
...evalCase,
configOverrides: {
...evalCase.configOverrides,
general: {
...evalCase.configOverrides?.general,
approvalMode: 'default',
enableAutoUpdate: false,
enableAutoUpdateNotification: false,
},
},
files: {
...evalCase.files,
},
});
}
describe('ask_user', () => {
evalTest('USUALLY_PASSES', {
askUserEvalTest('USUALLY_PASSES', {
name: 'Agent uses AskUser tool to present multiple choice options',
prompt: `Use the ask_user tool to ask me what my favorite color is. Provide 3 options: red, green, or blue.`,
setup: async (rig) => {
rig.setBreakpoint(['ask_user']);
},
assert: async (rig) => {
const wasToolCalled = await rig.waitForToolCall('ask_user');
expect(wasToolCalled, 'Expected ask_user tool to be called').toBe(true);
const confirmation = await rig.waitForPendingConfirmation('ask_user');
expect(
confirmation,
'Expected a pending confirmation for ask_user tool',
).toBeDefined();
},
});
evalTest('USUALLY_PASSES', {
askUserEvalTest('USUALLY_PASSES', {
name: 'Agent uses AskUser tool to clarify ambiguous requirements',
files: {
'package.json': JSON.stringify({ name: 'my-app', version: '1.0.0' }),
},
prompt: `I want to build a new feature in this app. Ask me questions to clarify the requirements before proceeding.`,
setup: async (rig) => {
rig.setBreakpoint(['ask_user']);
},
assert: async (rig) => {
const wasToolCalled = await rig.waitForToolCall('ask_user');
expect(wasToolCalled, 'Expected ask_user tool to be called').toBe(true);
const confirmation = await rig.waitForPendingConfirmation('ask_user');
expect(
confirmation,
'Expected a pending confirmation for ask_user tool',
).toBeDefined();
},
});
evalTest('USUALLY_PASSES', {
askUserEvalTest('USUALLY_PASSES', {
name: 'Agent uses AskUser tool before performing significant ambiguous rework',
files: {
'packages/core/src/index.ts': '// index\nexport const version = "1.0.0";',
@@ -39,28 +70,37 @@ describe('ask_user', () => {
}),
'README.md': '# Gemini CLI',
},
prompt: `Refactor the entire core package to be better.`,
prompt: `I want to completely rewrite the core package to support the upcoming V2 architecture, but I haven't decided what that looks like yet. We need to figure out the requirements first. Can you ask me some questions to help nail down the design?`,
setup: async (rig) => {
rig.setBreakpoint(['enter_plan_mode', 'ask_user']);
},
assert: async (rig) => {
const wasPlanModeCalled = await rig.waitForToolCall('enter_plan_mode');
expect(wasPlanModeCalled, 'Expected enter_plan_mode to be called').toBe(
true,
);
// It might call enter_plan_mode first.
let confirmation = await rig.waitForPendingConfirmation([
'enter_plan_mode',
'ask_user',
]);
expect(confirmation, 'Expected a tool call confirmation').toBeDefined();
if (confirmation?.name === 'enter_plan_mode') {
rig.acceptConfirmation('enter_plan_mode');
confirmation = await rig.waitForPendingConfirmation('ask_user');
}
const wasAskUserCalled = await rig.waitForToolCall('ask_user');
expect(
wasAskUserCalled,
'Expected ask_user tool to be called to clarify the significant rework',
).toBe(true);
confirmation?.toolName,
'Expected ask_user to be called to clarify the significant rework',
).toBe('ask_user');
},
});
// --- Regression Tests for Recent Fixes ---
// Regression test for issue #20177: Ensure the agent does not use `ask_user` to
// Regression test for issue #20177: Ensure the agent does not use \`ask_user\` to
// confirm shell commands. Fixed via prompt refinements and tool definition
// updates to clarify that shell command confirmation is handled by the UI.
// See fix: https://github.com/google-gemini/gemini-cli/pull/20504
evalTest('USUALLY_PASSES', {
askUserEvalTest('USUALLY_PASSES', {
name: 'Agent does NOT use AskUser to confirm shell commands',
files: {
'package.json': JSON.stringify({
@@ -68,25 +108,24 @@ describe('ask_user', () => {
}),
},
prompt: `Run 'npm run build' in the current directory.`,
setup: async (rig) => {
rig.setBreakpoint(['run_shell_command', 'ask_user']);
},
assert: async (rig) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const wasShellCalled = toolLogs.some(
(log) => log.toolRequest.name === 'run_shell_command',
);
const wasAskUserCalled = toolLogs.some(
(log) => log.toolRequest.name === 'ask_user',
);
const confirmation = await rig.waitForPendingConfirmation([
'run_shell_command',
'ask_user',
]);
expect(
wasShellCalled,
'Expected run_shell_command tool to be called',
).toBe(true);
confirmation,
'Expected a pending confirmation for a tool',
).toBeDefined();
expect(
wasAskUserCalled,
confirmation?.toolName,
'ask_user should not be called to confirm shell commands',
).toBe(false);
).toBe('run_shell_command');
},
});
});
+1 -1
View File
@@ -11,7 +11,7 @@ import { assertModelHasOutput } from '../integration-tests/test-helper.js';
describe('Hierarchical Memory', () => {
const conflictResolutionTest =
'Agent follows hierarchy for contradictory instructions';
evalTest('USUALLY_PASSES', {
evalTest('ALWAYS_PASSES', {
name: conflictResolutionTest,
params: {
settings: {
+3 -3
View File
@@ -14,7 +14,7 @@ import {
describe('save_memory', () => {
const TEST_PREFIX = 'Save memory test: ';
const rememberingFavoriteColor = "Agent remembers user's favorite color";
evalTest('USUALLY_PASSES', {
evalTest('ALWAYS_PASSES', {
name: rememberingFavoriteColor,
params: {
settings: { tools: { core: ['save_memory'] } },
@@ -79,7 +79,7 @@ describe('save_memory', () => {
const ignoringTemporaryInformation =
'Agent ignores temporary conversation details';
evalTest('USUALLY_PASSES', {
evalTest('ALWAYS_PASSES', {
name: ignoringTemporaryInformation,
params: {
settings: { tools: { core: ['save_memory'] } },
@@ -104,7 +104,7 @@ describe('save_memory', () => {
});
const rememberingPetName = "Agent remembers user's pet's name";
evalTest('USUALLY_PASSES', {
evalTest('ALWAYS_PASSES', {
name: rememberingPetName,
params: {
settings: { tools: { core: ['save_memory'] } },
@@ -0,0 +1,35 @@
# Future State: Gemini Workspaces Platform
This document outlines the long-term architectural evolution of the Workspaces feature (formerly "Workspace").
## 🎯 Vision
Transform Workspaces into a first-class platform capability that allows developers to seamlessly move intensive workloads (AI reasoning, complex builds, parallel testing) to any compute environment (Cloud or Local).
## 🗺️ Evolutionary Roadmap
### Phase 1: Generalization & Renaming (Current)
- **Goal**: Make the feature useful for any repository, not just Gemini CLI.
- **Action**: Rename to "Workspaces."
- **Action**: Implement dynamic repository detection via Git.
- **Action**: Isolate all state into `.gemini/workspaces/`.
### Phase 2: Pluggable Compute Extensions
- **Goal**: Decouple the infrastructure logic from the core CLI.
- **Action**: Move `WorkerProviders` into a dedicated **Workspaces Extension**.
- **Action**: Support multiple providers (GCP, AWS, Local Docker).
- **Action**: Define a standard API for Workspace Providers.
### Phase 3: Core Integration
- **Goal**: Standardize the user experience.
- **Action**: Move the high-level `gemini workspace` command into the core `gemini` binary.
- **Action**: Implement automated "Environment Hand-off" where the local agent can automatically spin up a remote workspace when it detects a heavy task.
### Phase 4: Public Marketplace
- **Goal**: Community adoption.
- **Action**: Publish the official GCP Workspace Extension.
- **Action**: Provide a "Zero-Config" public base image for standard Node/TS development.
## 🏗️ Architectural Principles
1. **BYOC (Bring Your Own Cloud)**: Users connect their own infrastructure.
2. **Nested Persistence**: Keep the environment in the container, but manage the lifecycle with the host.
3. **Repo-Agnostic**: One set of tools should work for any project.
+28
View File
@@ -0,0 +1,28 @@
# Architectural Mandate: High-Performance Workspace System
## Infrastructure Strategy
- **Base OS**: Always use **Container-Optimized OS (COS)** (`cos-stable` family). It is security-hardened and has Docker pre-installed.
- **Provisioning**: Use the **Cloud-Init (`user-data`)** pattern.
- *Note*: Avoid `gcloud compute instances create-with-container` on standard Linux images as it uses a deprecated startup agent. On COS, use native `user-data` for cleanest execution.
- **Performance**: Provision with a minimum of **200GB PD-Balanced** disk to ensure high I/O throughput for Node.js builds and to satisfy GCP disk performance requirements.
## Container Isolation
- **Image**: `us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest`.
- **Identity**: The container must be named **`maintainer-worker`**.
- **Mounts**: Standardize on these host-to-container mappings:
- `~/dev` -> `/home/node/dev` (Persistence for worktrees)
- `~/.gemini` -> `/home/node/.gemini` (Shared credentials)
- `~/.workspace` -> `/home/node/.workspace` (Shared scripts/logs)
- **Runtime**: The container runs as a persistent service (`--restart always`) acting as a "Remote Workstation" rather than an ephemeral task.
## Orchestration Logic
- **Worker Provider Abstraction**: Infrastructure is managed via a `WorkerProvider` interface (e.g., `GceCosProvider`). This decouples the orchestration logic from the underlying platform.
- **Robust Connectivity**: The system uses a dual-path connectivity strategy:
1. **Fast-Path SSH**: Primary connection via a standard SSH alias (`gcli-worker`) for high-performance synchronization and interaction.
2. **IAP Fallback**: Automatic fallback to `gcloud compute ssh --tunnel-through-iap` for users off-VPC or when direct DNS resolution fails.
- **Context Execution**: Use `docker exec -it maintainer-worker ...` for interactive tasks and `tmux` sessions. This provides persistence against connection drops while keeping the host OS "invisible."
- **Path Resolution**: Both Host and Container must share identical tilde (`~`) paths to avoid mapping confusion in automation scripts.
## Maintenance
- **Rebuilds**: If the environment drifts or the image updates, delete the VM and re-run the `provision` action.
- **Status**: The Mission Control dashboard derives state by scanning host `tmux` sessions and container filesystem logs.
@@ -0,0 +1,49 @@
# Network Architecture & Troubleshooting Research
This document captures the empirical research and final configuration settled upon for the Gemini CLI Workspace system, specifically addressing the challenges of connecting from corporate environments to private GCP workers.
## 🔍 The Challenge
The goal was to achieve **Direct internal SSH** access to GCE workers that have **no public IP addresses**, allowing for high-performance file synchronization (`rsync`) and interactive sessions without the overhead of `gcloud` wrappers.
## 🧪 What Was Tested
### 1. Standard Internal DNS (`<instance>.<zone>.c.<project>.internal`)
- **Result**: ❌ FAILED
- **Observation**: Standard GCE internal DNS suffixes often fail to resolve or route correctly from local workstations in certain corporate environments, even when VPN/Peering is active.
### 2. IAP Tunneling (`gcloud compute ssh --tunnel-through-iap`)
- **Result**: ⚠️ INCONSISTENT
- **Observation**: While IAP is the standard fallback for private VMs, it failed with "failed to connect to backend" (4003) when the underlying VPC network lacked proper configuration or when firewall rules were misaligned with the specific network interface.
### 3. Custom "Auto" Networks
- **Result**: ❌ FAILED
- **Observation**: Creating a fresh VPC with default "auto" settings was insufficient. The "magic" corporate routing paths did not automatically extend to these new, isolated networks.
## ✅ The Final State (The "Magic" Configuration)
Through comparison with the working `gemini-cli-team-quota` project and empirical testing in a sandbox, we settled on the following requirements:
### 1. Hostname Construction
The system **MUST** use the following specific hostname pattern for direct internal reachability:
`nic0.<instance>.<zone>.c.<project>.internal.gcpnode.com`
### 2. VPC Configuration
The VPC (e.g., `iap-vpc`) must be a **Custom Mode** network with the following properties:
- **Private Google Access**: MUST be enabled on the subnetwork. This allows the private VM to communicate with Google services (like Artifact Registry) without a public IP.
- **Firewall Rule**: An ingress rule allowing `tcp:22` from `0.0.0.0/0`.
- *Note*: While `0.0.0.0/0` seems broad, in this context it is typically restricted by the corporate-level gateway/peering that provides the `internal.gcpnode.com` route.
### 3. Worker Provider Abstraction
To manage this complexity, we implemented a `WorkerProvider` architecture:
- **`BaseProvider`**: Defines a common interface for `exec`, `sync`, and `provision`.
- **`GceCosProvider`**: Encapsulates the GCE-specific "magic" (hostname construction, IAP fallbacks, COS startup scripts).
## 🛠️ Why This Works
This configuration aligns with the **Google Corporate Direct-Access** pattern. By using the `nic0` prefix and the `.gcpnode.com` suffix, the connection is routed through internal corporate proxies that recognize the authenticated developer identity and permit the direct SSH handshake to the private IP.
## 📜 Technical Metadata Summary
- **Network**: `iap-vpc` (Custom)
- **Subnet**: `iap-subnet` (Private Google Access: Enabled)
- **Identity**: OS Login (`enable-oslogin=TRUE`)
- **Image**: Container-Optimized OS (COS)
- **Connectivity**: Direct SSH via `nic0` -> Automatic Fallback to IAP.
@@ -0,0 +1,60 @@
# Mission: GCE Container-First Refactor 🚀
## Current State
- **Architecture**: Persistent GCE VM (`gcli-workspace-mattkorwel`) with Fast-Path SSH (`gcli-worker`).
- **Logic**: Decoupled scripts in `~/.workspace/scripts`, using Git Worktrees for concurrency.
- **Auth**: Scoped GitHub PATs mirrored via setup.
## The Goal (Container-OS Transition)
Shift from a "Manual VM" to an "Invisible VM" (Container-Optimized OS) that runs our Sandbox Docker image directly.
## Planned Changes
1. **Multi-Stage Dockerfile**: ✅ VERIFIED
- Optimize `.gcp/Dockerfile.maintainer` to include `tsx`, `vitest`, `gh`, and system dependencies (`libsecret`, `build-essential`).
- *Verified locally: Node v20, GH CLI, Git, TSX, and Vitest are functional with required headers.*
2. **Dedicated Pipeline**:
- Use `.gcp/maintainer-worker.yml` for isolated builds.
- **Tagging Strategy**:
- `latest`: Automatically updated on every merge to `main`.
- `branch-name`: Created on-demand for PRs via `/gcbrun` comment.
3. **Setup Script (`setup.ts`)**:
- Refactor `provision` to use `gcloud compute instances create-with-container`.
- Point to the new `maintainer` image in Artifact Registry.
4. **Orchestrator (`orchestrator.ts`)**:
- Update SSH logic to include the `--container` flag.
## GCP Console Setup (Two Triggers)
### Trigger 1: Production Maintainer Image (Automatic)
1. **Event**: Push to branch.
2. **Branch**: `^main$`.
3. **Configuration**: Point to `.gcp/maintainer-worker.yml`.
4. **Purpose**: Keeps the stable "Golden Image" up to date for daily use.
### Trigger 2: On-Demand Testing (Comment-Gated)
1. **Event**: Pull request.
2. **Base Branch**: `^main$`.
3. **Comment Control**: Set to **"Required"** (e.g. `/gcbrun`).
4. **Configuration**: Point to `.gcp/maintainer-worker.yml`.
5. **Purpose**: Allows developers to test infrastructure changes before merging.
## Phase 2: Refactoring setup.ts for Container-OS
This phase is currently **ARCHIVED** in favor of the Persistent Workstation model.
### Implementation Logic (Snapshot)
The orchestrator should launch isolated containers using this pattern:
```bash
docker run --rm -it \
--name workspace-job-id \
-v ~/dev/worktrees/job-id:/home/node/dev/worktree:rw \
-v ~/dev/main:/home/node/dev/main:ro \
-v ~/.gemini:/home/node/.gemini:ro \
-w /home/node/dev/worktree \
maintainer-image:latest \
sh -c "tsx ~/.workspace/scripts/entrypoint.ts ..."
```
## How to Resume
1. Review the archived container-launch logic above.
2. Update `setup.ts` to use `gcloud compute instances create-with-container`.
3. Update `orchestrator.ts` to use `docker run` instead of standard `ssh`.
+107
View File
@@ -0,0 +1,107 @@
# Workspace maintainer skill
The `workspace` skill provides a high-performance, parallelized workflow for
workspaceing intensive developer tasks to a remote workstation. It leverages a
Node.js orchestrator to run complex validation playbooks concurrently in a
dedicated terminal window.
## Why use workspace?
As a maintainer, you eventually reach the limits of how much work you can manage
at once on a single local machine. Heavy builds, concurrent test suites, and
multiple PRs in flight can quickly overload local resources, leading to
performance degradation and developer friction.
While manual remote management is a common workaround, it is often cumbersome
and context-heavy. The `workspace` skill addresses these challenges by providing:
- **Elastic compute**: Workspace resource-intensive build and lint suites to a
beefy remote workstation, keeping your local machine responsive.
- **Context preservation**: The main Gemini session remains interactive and
focused on high-level reasoning while automated tasks provide real-time
feedback in a separate window.
- **Automated orchestration**: The skill handles worktree provisioning,
script synchronization, and environment isolation automatically.
- **True parallelism**: Infrastructure validation, CI checks, and behavioral
proofs run simultaneously, compressing a 15-minute process into 3 minutes.
## Agentic skills: Sync or Workspace
The `workspace` system is designed to work in synergy with specialized agentic
skills. These skills can be run **synchronously** in your current terminal for
quick tasks, or **workspaceed** to a remote session for complex, iterative loops.
- **`review-pr`**: Conducts high-fidelity, behavioral code reviews. It assumes
the infrastructure is already validated and focuses on physical proof of
functionality.
- **`fix-pr`**: An autonomous "Fix-to-Green" loop. It iteratively addresses
CI failures, merge conflicts, and review comments until the PR is mergeable.
When you run `npm run workspace <PR> fix`, the orchestrator provisions the remote
environment and then launches a Gemini CLI session specifically powered by the
`fix-pr` skill.
## Architecture: The Hybrid Powerhouse
The workspace system uses a **Hybrid VM + Docker** architecture designed for maximum performance and reliability:
1. **The GCE VM (Raw Power)**: By running on high-performance Google Compute Engine instances, we workspace heavy CPU and RAM tasks (like full project builds and massive test suites) from your local machine, keeping your primary workstation responsive.
2. **The Docker Container (Consistency & Resilience)**:
* **Source of Truth**: The `.gcp/Dockerfile.maintainer` defines the exact environment. If a tool is added there, every maintainer gets it instantly.
* **Zero Drift**: Containers are immutable. Every job starts in a fresh state, preventing the "OS rot" that typically affects persistent VMs.
* **Local-to-Remote Parity**: The same image can be run locally on your Mac or remotely in GCP, ensuring that "it works on my machine" translates 100% to the remote worker.
* **Safe Multi-tenancy**: Using Git Worktrees inside an isolated container environment allows multiple jobs to run in parallel without sharing state or polluting the host system.
## Playbooks
- **`review`** (default): Build, CI check, static analysis, and behavioral proofs.
- **`fix`**: Iterative fixing of CI failures and review comments.
- **`ready`**: Final full validation (clean install + preflight) before merge.
- **`open`**: Provision a worktree and drop directly into a remote tmux session.
## Scenario and workflows
### Getting Started (Onboarding)
For a complete guide on setting up your remote environment, see the [Maintainer Onboarding Guide](../../../MAINTAINER_ONBOARDING.md).
### Persistence and Job Recovery
The workspace system is designed for high reliability and persistence. Jobs use a nested execution model to ensure they continue running even if your local terminal is closed or the connection is lost.
### How it Works
1. **Host-Level Persistence**: The orchestrator launches each job in a named **`tmux`** session on the remote VM.
2. **Container Isolation**: The actual work is performed inside the persistent `maintainer-worker` Docker container.
### Re-attaching to a Job
If you lose your connection, you can easily resume your session:
- **Automatic**: Simply run the exact same command you started with (e.g., `npm run workspace 123 review`). The system will automatically detect the existing session and re-attach you.
- **Manual**: Use `npm run workspace:status` to find the session name, then use `ssh gcli-worker` to jump into the VM and `tmux attach -t <session>` to resume.
## Technical details
This skill uses a **Worker Provider** abstraction (`GceCosProvider`) to manage the remote lifecycle. It uses an isolated Gemini profile on the remote host (`~/.workspace/gemini-cli-config`) to ensure that verification tasks do not interfere with your primary configuration.
### Directory structure
- `scripts/providers/`: Modular worker implementations (GCE, etc.).
- `scripts/orchestrator.ts`: Local orchestrator (syncs scripts and pops terminal).
- `scripts/worker.ts`: Remote engine (provisions worktree and runs playbooks).
- `scripts/check.ts`: Local status poller.
- `scripts/clean.ts`: Remote cleanup utility.
- `SKILL.md`: Instructional body used by the Gemini CLI agent.
## Contributing
If you want to improve this skill:
1. Modify the TypeScript scripts in `scripts/`.
2. Update `SKILL.md` if the agent's instructions need to change.
3. Test your changes locally using `npm run workspace <PR>`.
## Testing
The orchestration logic for this skill is fully tested. To run the tests:
```bash
npx vitest .gemini/skills/workspace/tests/orchestration.test.ts
```
These tests mock the external environment (SSH, GitHub CLI, and the file system) to ensure that the orchestration scripts generate the correct commands and handle environment isolation accurately.
@@ -0,0 +1,46 @@
# Plan: Worker Provider Abstraction for Workspace System
## Objective
Abstract the remote execution infrastructure (GCE COS, GCE Linux, Cloud Workstations) behind a common `WorkerProvider` interface. This eliminates infrastructure-specific prompts (like "use container mode") and makes the system extensible to new backends.
## Architectural Changes
### 1. New Provider Abstraction
Create a modular provider system where each infrastructure type implements a standard interface.
- **Base Interface**: `WorkerProvider` (methods for `exec`, `sync`, `provision`, `getStatus`).
- **Implementations**:
- `GceCosProvider`: Handles COS with Cloud-Init and `docker exec` wrapping.
- `GceLinuxProvider`: Handles standard Linux VMs with direct execution.
- `LocalDockerProvider`: (Future) Runs workspace tasks in a local container.
- `WorkstationProvider`: (Future) Integrates with Google Cloud Workstations.
### 2. Auto-Discovery
Modify `setup.ts` to:
- Prompt for a high-level "Provider Type" (e.g., "Google Cloud (COS)", "Google Cloud (Linux)").
- Auto-detect environment details where possible (e.g., fetching internal IPs, identifying container names).
### 3. Clean Orchestration
Refactor `orchestrator.ts` to be provider-agnostic:
- It asks the provider to "Ensure Ready" (wake VM).
- It asks the provider to "Prepare Environment" (worktree setup).
- It asks the provider to "Launch Task" (tmux initialization).
## Implementation Steps
### Phase 1: Infrastructure Cleanup
- Move existing procedural logic from `fleet.ts`, `setup.ts`, and `orchestrator.ts` into a new `providers/` directory.
- Create `ProviderFactory` to instantiate the correct implementation based on `settings.json`.
### Phase 2: Refactor Scripts
- **`fleet.ts`**: Proxy all actions (`provision`, `rebuild`, `stop`) to the provider.
- **`orchestrator.ts`**: Use the provider for the entire lifecycle of a job.
- **`status.ts`**: Use the provider's `getStatus()` method to derive state.
### Phase 3: Validation
- Verify that the `gcli-worker` SSH alias and IAP tunneling remain functional.
- Ensure "Fast-Path SSH" is still the primary interactive gateway.
## Verification
- Run `npm run workspace:fleet provision` and ensure it creates a COS-native worker.
- Run `npm run workspace:setup` and verify it no longer asks cryptic infrastructure questions.
- Launch a review and verify it uses `docker exec internally for the COS provider.
@@ -0,0 +1,95 @@
/**
* Shared Task Runner Utility
* Handles parallel process execution, log streaming, and dashboard rendering.
*/
import { spawn } from 'child_process';
import path from 'path';
import fs from 'fs';
export interface Task {
id: string;
name: string;
cmd: string;
dep?: string;
condition?: 'success' | 'fail';
}
export class TaskRunner {
private state: Record<string, { status: string; exitCode?: number }> = {};
private tasks: Task[] = [];
private logDir: string;
private header: string;
constructor(logDir: string, header: string) {
this.logDir = logDir;
this.header = header;
fs.mkdirSync(logDir, { recursive: true });
}
register(tasks: Task[]) {
this.tasks = tasks;
tasks.forEach(t => this.state[t.id] = { status: 'PENDING' });
}
async run() {
const runQueue = this.tasks.filter(t => !t.dep);
runQueue.forEach(t => this.execute(t));
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
const allDone = this.tasks.every(t =>
['SUCCESS', 'FAILED', 'SKIPPED'].includes(this.state[t.id].status)
);
if (allDone) {
clearInterval(checkInterval);
console.log('\n✨ All tasks complete.');
resolve(this.state);
}
// Check for dependencies
this.tasks.filter(t => t.dep && this.state[t.id].status === 'PENDING').forEach(t => {
const parent = this.state[t.dep!];
if (parent.status === 'SUCCESS' && (!t.condition || t.condition === 'success')) {
this.execute(t);
} else if (parent.status === 'FAILED' && t.condition === 'fail') {
this.execute(t);
} else if (['SUCCESS', 'FAILED'].includes(parent.status)) {
this.state[t.id].status = 'SKIPPED';
}
});
this.render();
}, 1500);
});
}
private execute(task: Task) {
this.state[task.id].status = 'RUNNING';
const proc = spawn(task.cmd, { shell: true, env: { ...process.env, FORCE_COLOR: '1' } });
const logStream = fs.createWriteStream(path.join(this.logDir, `${task.id}.log`));
proc.stdout.pipe(logStream);
proc.stderr.pipe(logStream);
proc.on('close', (code) => {
const exitCode = code ?? 0;
this.state[task.id].status = exitCode === 0 ? 'SUCCESS' : 'FAILED';
this.state[task.id].exitCode = exitCode;
fs.writeFileSync(path.join(this.logDir, `${task.id}.exit`), exitCode.toString());
});
}
private render() {
console.clear();
console.log('==================================================');
console.log(this.header);
console.log('==================================================\n');
this.tasks.forEach(t => {
const s = this.state[t.id];
const icon = s.status === 'SUCCESS' ? '✅' : s.status === 'FAILED' ? '❌' : s.status === 'RUNNING' ? '⏳' : s.status === 'SKIPPED' ? '⏭️ ' : '💤';
console.log(` ${icon} ${t.name.padEnd(20)}: ${s.status}`);
});
}
}
+78
View File
@@ -0,0 +1,78 @@
/**
* Workspace Attach Utility (Local)
*
* Re-attaches to a running tmux session inside the container on the worker.
*/
import path from 'path';
import fs from 'fs';
import { spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
const q = (str: string) => `'${str.replace(/'/g, "'\\''")}'`;
export async function runAttach(args: string[], env: NodeJS.ProcessEnv = process.env) {
const prNumber = args[0];
const action = args[1] || 'review';
const isLocal = args.includes('--local');
if (!prNumber) {
console.error('Usage: npm run workspace:attach <PR_NUMBER> [action] [--local]');
return 1;
}
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Settings not found. Run "npm run workspace:setup" first.');
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const config = settings.workspace;
if (!config) {
console.error('❌ Deep Review configuration not found.');
return 1;
}
const { projectId, zone } = config;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
const sessionName = `workspace-${prNumber}-${action}`;
const containerAttach = `sudo docker exec -it maintainer-worker sh -c ${q(`tmux attach-session -t ${sessionName}`)}`;
const finalSSH = provider.getRunCommand(containerAttach, { interactive: true });
console.log(`🔗 Attaching to session: ${sessionName}...`);
const isWithinGemini = !!env.GEMINI_CLI || !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID;
if (isWithinGemini && !isLocal) {
const tempCmdPath = path.join(process.env.TMPDIR || '/tmp', `workspace-attach-${prNumber}.sh`);
fs.writeFileSync(tempCmdPath, `#!/bin/bash\n${finalSSH}\nrm "$0"`, { mode: 0o755 });
const appleScript = `
on run argv
tell application "iTerm"
tell current window
set newTab to (create tab with default profile)
tell current session of newTab
write text (item 1 of argv) & return
end tell
end tell
activate
end tell
end run
`;
spawnSync('osascript', ['-', tempCmdPath], { input: appleScript });
console.log(`✅ iTerm2 tab opened for ${sessionName}.`);
return 0;
}
spawnSync(finalSSH, { stdio: 'inherit', shell: true });
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runAttach(process.argv.slice(2)).catch(console.error);
}
+70
View File
@@ -0,0 +1,70 @@
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
export async function runChecker(args: string[], env: NodeJS.ProcessEnv = process.env) {
const prNumber = args[0];
if (!prNumber) {
console.error('Usage: npm run review:check <PR_NUMBER>');
return 1;
}
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Settings not found. Run "npm run workspace:setup" first.');
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const config = settings.workspace;
if (!config) {
console.error('❌ Deep Review configuration not found.');
return 1;
}
const { projectId, zone, remoteWorkDir } = config;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
console.log(`🔍 Checking remote status for PR #${prNumber} on ${targetVM}...`);
const branchView = spawnSync('gh', ['pr', 'view', prNumber, '--json', 'headRefName', '-q', '.headRefName'], { shell: true });
const branchName = branchView.stdout.toString().trim();
const logDir = `${remoteWorkDir}/${branchName}/.gemini/logs/review-${prNumber}`;
const tasks = ['build', 'ci', 'review', 'verify'];
let allDone = true;
console.log('\n--- Task Status ---');
for (const task of tasks) {
const exitFile = `${logDir}/${task}.exit`;
const checkExit = await provider.getExecOutput(`[ -f ${exitFile} ] && cat ${exitFile}`, { wrapContainer: 'maintainer-worker' });
if (checkExit.status === 0 && checkExit.stdout.trim()) {
const code = checkExit.stdout.trim();
console.log(` ${code === '0' ? '✅' : '❌'} ${task.padEnd(10)}: ${code === '0' ? 'SUCCESS' : `FAILED (exit ${code})`}`);
} else {
const checkRunning = await provider.exec(`[ -f ${logDir}/${task}.log ]`, { wrapContainer: 'maintainer-worker' });
if (checkRunning === 0) {
console.log(`${task.padEnd(10)}: RUNNING`);
} else {
console.log(` 💤 ${task.padEnd(10)}: PENDING`);
}
allDone = false;
}
}
if (allDone) {
console.log('\n✨ All remote tasks complete. You can now synthesize the results.');
} else {
console.log('\n⏳ Some tasks are still in progress. Check again in a few minutes.');
}
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runChecker(process.argv.slice(2)).catch(console.error);
}
+99
View File
@@ -0,0 +1,99 @@
/**
* Universal Workspace Cleanup (Local)
*
* Surgical or full cleanup of sessions and worktrees on the GCE worker.
* Refactored to use WorkerProvider for container compatibility.
*/
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import readline from 'readline';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
async function confirm(question: string): Promise<boolean> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${question} (y/n): `, (answer) => {
rl.close();
resolve(answer.trim().toLowerCase() === 'y');
});
});
}
export async function runCleanup(args: string[], env: NodeJS.ProcessEnv = process.env) {
const prNumber = args[0];
const action = args[1];
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Settings not found. Run "npm run workspace:setup" first.');
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const config = settings.workspace;
if (!config) {
console.error('❌ Workspace configuration not found.');
return 1;
}
const { projectId, zone } = config;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
if (prNumber && action) {
const sessionName = `workspace-${prNumber}-${action}`;
const worktreePath = `/home/node/.workspaces/worktrees/${sessionName}`;
console.log(`🧹 Surgically removing session and worktree for ${prNumber}-${action}...`);
// Kill specific tmux session inside container
await provider.exec(`tmux kill-session -t ${sessionName} 2>/dev/null`, { wrapContainer: 'maintainer-worker' });
// Remove specific worktree inside container
await provider.exec(`cd /home/node/.workspaces/main && git worktree remove -f ${worktreePath} 2>/dev/null && git worktree prune`, { wrapContainer: 'maintainer-worker' });
console.log(`✅ Cleaned up ${prNumber}-${action}.`);
return 0;
}
// --- Bulk Cleanup ---
console.log(`⚠️ DANGER: You are about to perform a BULK cleanup on ${targetVM}.`);
const confirmed = await confirm(' Are you sure you want to kill ALL sessions and worktrees?');
if (!confirmed) {
console.log('❌ Cleanup cancelled.');
return 0;
}
console.log(`🧹 Starting BULK cleanup...`);
// 1. Standard Cleanup
console.log(' - Killing ALL remote tmux sessions...');
await provider.exec(`tmux kill-server`, { wrapContainer: 'maintainer-worker' });
console.log(' - Cleaning up Docker resources...');
await provider.exec(`sudo docker rm -f maintainer-worker || true`);
await provider.exec(`sudo docker system prune -af --volumes`);
console.log(' - Cleaning up ALL Git Worktrees...');
await provider.exec(`cd /home/node/.workspaces/main && git worktree prune && rm -rf /home/node/.workspaces/worktrees/*`, { wrapContainer: 'maintainer-worker' });
console.log('✅ Remote environment cleared.');
// 2. Full Wipe Option
const shouldWipe = await confirm('\nWould you like to COMPLETELY wipe the remote workspace (main clone)?');
if (shouldWipe) {
console.log(`🔥 Wiping /home/node/.workspaces/main...`);
await provider.exec(`rm -rf /home/node/.workspaces/main && mkdir -p /home/node/.workspaces/main`, { wrapContainer: 'maintainer-worker' });
console.log('✅ Remote hub wiped. You will need to run npm run workspace:setup again.');
}
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runCleanup(process.argv.slice(2)).catch(console.error);
}
@@ -0,0 +1,61 @@
/**
* Deep Review Entrypoint (Remote)
*
* This script is the single command executed by the remote tmux session.
* It handles environment loading and sequence orchestration.
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const prNumber = process.argv[2];
const branchName = process.argv[3];
const policyPath = process.argv[4];
const ISOLATED_CONFIG = process.env.GEMINI_CLI_HOME || path.join(process.env.HOME || '', '.workspaces/gemini-cli-config');
async function main() {
if (!prNumber || !branchName || !policyPath) {
console.error('Usage: tsx entrypoint.ts <PR_NUMBER> <BRANCH_NAME> <POLICY_PATH>');
process.exit(1);
}
const workDir = process.cwd(); // This is remoteWorkDir as set in review.ts
const targetDir = path.join(workDir, branchName);
// Use global tools pre-installed in the maintainer image
const tsxBin = 'tsx';
const geminiBin = 'gemini';
const action = process.argv[5] || 'review';
// 1. Run the Parallel Reviewer
console.log('🚀 Launching Parallel Review Worker...');
console.log(` - Script: ${path.join(__dirname, 'worker.ts')}`);
console.log(` - Action: ${action}`);
const workerResult = spawnSync(tsxBin, [path.join(__dirname, 'worker.ts'), prNumber, branchName, policyPath, action], {
stdio: 'inherit',
env: { ...process.env, GEMINI_CLI_HOME: ISOLATED_CONFIG }
});
if (workerResult.status !== 0) {
console.error(`❌ Worker failed with exit code ${workerResult.status}.`);
if (workerResult.error) console.error(' Error:', workerResult.error.message);
}
// 2. Launch the Interactive Gemini Session (Local Nightly)
console.log('\n✨ Verification complete. Joining interactive session...');
const geminiArgs = ['--policy', policyPath];
geminiArgs.push('-p', `Review for PR #${prNumber} is complete. Read the logs in .gemini/logs/review-${prNumber}/ and synthesize your findings.`);
process.chdir(targetDir);
spawnSync(geminiBin, geminiArgs, {
stdio: 'inherit',
env: { ...process.env, GEMINI_CLI_HOME: ISOLATED_CONFIG }
});
}
main().catch(console.error);
+117
View File
@@ -0,0 +1,117 @@
/**
* Workspace Fleet Manager
*
* Manages dynamic GCP workers for workspaces tasks.
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
const USER = process.env.USER || 'mattkorwel';
const INSTANCE_PREFIX = `gcli-workspace-${USER}`;
const DEFAULT_ZONE = 'us-west1-a';
function getProjectId(): string {
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (fs.existsSync(settingsPath)) {
try {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
return settings.workspace?.projectId;
} catch (e) {}
}
return process.env.GOOGLE_CLOUD_PROJECT || '';
}
async function listWorkers() {
const projectId = getProjectId();
if (!projectId) {
console.error('❌ Project ID not found. Run "npm run workspace:setup" first.');
return;
}
console.log(`🔍 Listing Workspace Workers for ${USER} in ${projectId}...`);
spawnSync('gcloud', [
'compute', 'instances', 'list',
'--project', projectId,
'--filter', `name~^${INSTANCE_PREFIX}`,
'--format', 'table(name,zone,status,networkInterfaces[0].networkIP:label=INTERNAL_IP,creationTimestamp)'
], { stdio: 'inherit' });
}
async function provisionWorker() {
const projectId = getProjectId();
if (!projectId) {
console.error('❌ Project ID not found. Run "npm run workspace:setup" first.');
return;
}
const provider = ProviderFactory.getProvider({
projectId: projectId,
zone: DEFAULT_ZONE,
instanceName: INSTANCE_PREFIX
});
const status = await provider.getStatus();
if (status.status !== 'UNKNOWN' && status.status !== 'ERROR') {
console.log(`✅ Worker ${INSTANCE_PREFIX} already exists and is ${status.status}.`);
return;
}
await provider.provision();
}
async function stopWorker() {
const projectId = getProjectId();
const provider = ProviderFactory.getProvider({
projectId: projectId,
zone: DEFAULT_ZONE,
instanceName: INSTANCE_PREFIX
});
console.log(`🛑 Stopping workspace worker: ${INSTANCE_PREFIX}...`);
await provider.stop();
}
async function rebuildWorker() {
const projectId = getProjectId();
console.log(`🔥 Rebuilding worker ${INSTANCE_PREFIX}...`);
const knownHostsPath = path.join(REPO_ROOT, '.gemini/workspaces_known_hosts');
if (fs.existsSync(knownHostsPath)) {
console.log(` - Clearing isolated known_hosts...`);
fs.unlinkSync(knownHostsPath);
}
spawnSync('gcloud', ['compute', 'instances', 'delete', INSTANCE_PREFIX, '--project', projectId, '--zone', DEFAULT_ZONE, '--quiet'], { stdio: 'inherit' });
await provisionWorker();
}
async function main() {
const action = process.argv[2] || 'list';
switch (action) {
case 'list':
await listWorkers();
break;
case 'provision':
await provisionWorker();
break;
case 'rebuild':
await rebuildWorker();
break;
case 'stop':
await stopWorker();
break;
default:
console.error(`❌ Unknown fleet action: ${action}`);
process.exit(1);
}
}
main().catch(console.error);
+51
View File
@@ -0,0 +1,51 @@
/**
* Workspace Log Tailer (Local)
*
* Tails the latest remote logs for a specific job.
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
export async function runLogs(args: string[]) {
const prNumber = args[0];
const action = args[1] || 'review';
if (!prNumber) {
console.error('Usage: npm run workspace:logs <PR_NUMBER> [action]');
return 1;
}
const settingsPath = path.join(REPO_ROOT, '.gemini/settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const config = settings.maintainer?.workspace;
const { remoteHost, remoteHome } = config;
const sshConfigPath = path.join(REPO_ROOT, '.gemini/workspace_ssh_config');
const jobDir = `${remoteHome}/dev/worktrees/workspace-${prNumber}-${action}`;
const logDir = `${jobDir}/.gemini/logs`;
console.log(`📋 Tailing latest logs for job ${prNumber}-${action}...`);
// Remote command to find the latest log file and tail it
const tailCmd = `
latest_log=$(ls -t ${logDir}/*.log 2>/dev/null | head -n 1)
if [ -z "$latest_log" ]; then
echo "❌ No logs found for this job yet."
exit 1
fi
echo "📄 Tailing: $latest_log"
tail -f "$latest_log"
`;
spawnSync(`ssh -F ${sshConfigPath} ${remoteHost} ${JSON.stringify(tailCmd)}`, { stdio: 'inherit', shell: true });
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runLogs(process.argv.slice(2)).catch(console.error);
}
@@ -0,0 +1,205 @@
/**
* Workspace Orchestrator (Local)
*
* Central coordination of remote tasks.
* Wakes workers, prepares worktrees, and launches tmux sessions.
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
function q(str: string) {
return `'${str.replace(/'/g, "'\\''")}'`;
}
export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = process.env) {
let prNumber = args[0];
let action = args[1] || 'review';
// Handle "shell" mode: npm run workspace:shell [identifier]
const isShellMode = prNumber === 'shell';
if (isShellMode) {
prNumber = args[1] || `adhoc-${Math.floor(Math.random() * 10000)}`;
action = 'shell';
}
if (!prNumber) {
console.error('❌ Usage: npm run workspace <PR_NUMBER> [action] OR npm run workspace:shell [identifier]');
return 1;
}
// 1. Load Settings
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Workspace settings not found. Run "npm run workspace:setup" first.');
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const config = settings.workspace;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId: config.projectId, zone: config.zone, instanceName: targetVM });
// 2. Wake Worker & Verify Container
await provider.ensureReady();
// Retrieve the remote user to ensure we run git commands correctly
const whoamiRes = await provider.getExecOutput('whoami');
const remoteUser = whoamiRes.stdout.trim();
// Paths - Unified across host and container
const hostWorkspaceRoot = `/home/node/.workspaces`;
const hostWorkDir = `${hostWorkspaceRoot}/main`;
const containerHome = '/home/node';
const containerWorkspaceRoot = `/home/node/.workspaces`;
const remotePolicyPath = `${containerWorkspaceRoot}/policies/workspace-policy.toml`;
const persistentScripts = `${containerWorkspaceRoot}/scripts`;
const sessionName = `workspace-${prNumber}-${action}`;
const remoteWorktreeDir = `${containerWorkspaceRoot}/worktrees/${sessionName}`;
const hostWorktreeDir = `${hostWorkspaceRoot}/worktrees/${sessionName}`;
// 3. Remote Context Setup (Executed on HOST for permission simplicity)
console.log(`🚀 Preparing remote environment for ${action} on ${isShellMode ? 'branch/id' : '#'}${prNumber}...`);
// FIX: Use the host path to check for existence
const check = await provider.getExecOutput(`ls -d ${hostWorktreeDir}/.git`);
// FIX: Ensure container user (node) owns the workspaces directories
console.log(' - Synchronizing container permissions...');
await provider.exec(`sudo chown -R 1000:1000 /home/node/.workspaces`);
if (check.status !== 0) {
console.log(` - Provisioning isolated git worktree for ${prNumber}...`);
// We run these on the host. Since setup might have left the repo root-owned, we use sudo.
// We use environment variables to bypass safe.directory checks on a read-only filesystem.
const gitEnv = `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0=${hostWorkDir}`;
const gitFetch = isShellMode
? `sudo ${gitEnv} git -C ${hostWorkDir} fetch --quiet origin`
: `sudo ${gitEnv} git -C ${hostWorkDir} fetch --quiet upstream pull/${prNumber}/head`;
const gitTarget = isShellMode ? 'FETCH_HEAD' : 'FETCH_HEAD';
const setupCmd = `
sudo mkdir -p ${hostWorkspaceRoot}/worktrees && \
sudo chown chronos:chronos ${hostWorkspaceRoot}/worktrees && \
${gitFetch} && \
sudo ${gitEnv} git -C ${hostWorkDir} worktree add --quiet -f ${hostWorktreeDir} ${gitTarget} 2>&1 && \
sudo chown -R 1000:1000 ${hostWorkspaceRoot}
`;
const setupRes = await provider.getExecOutput(setupCmd);
if (setupRes.status !== 0) {
console.error(' ❌ Failed to provision remote worktree.');
console.error(' STDOUT:', setupRes.stdout);
console.error(' STDERR:', setupRes.stderr);
return 1;
}
console.log(' ✅ Worktree provisioned successfully.');
} else {
console.log(' ✅ Remote worktree ready.');
}
// AUTH: Dynamically retrieve credentials from host-side config/disk
const remoteConfigPath = `${hostWorkspaceRoot}/gemini-cli-config/.gemini/settings.json`;
const remoteSettingsRes = await provider.getExecOutput(`cat ${remoteConfigPath}`);
const remoteSettingsJson = remoteSettingsRes.stdout.trim();
const apiKeyRes = await provider.getExecOutput(`cat ${remoteConfigPath} | grep apiKey | cut -d '\"' -f 4`);
const remoteApiKey = apiKeyRes.stdout.trim();
const ghTokenRes = await provider.getExecOutput(`cat ${hostWorkspaceRoot}/.gh_token`);
const remoteGhToken = ghTokenRes.stdout.trim();
// AUTH: Inject credentials and settings directly into the worktree
console.log(' - Injecting remote authentication and UI context...');
const dotEnvContent = `
GEMINI_API_KEY=${remoteApiKey}
COLORTERM=truecolor
TERM=xterm-256color
GEMINI_AUTO_UPDATE=0
GEMINI_SANDBOX=workspace
GEMINI_HOST=${targetVM}
`.trim();
await provider.exec(`sudo docker exec maintainer-worker sh -c ${q(`echo ${q(dotEnvContent)} > ${remoteWorktreeDir}/.env`)}`);
// Also inject the settings.json into the worktree's .gemini folder for maximum reliability
await provider.exec(`sudo docker exec maintainer-worker sh -c ${q(`mkdir -p ${remoteWorktreeDir}/.gemini && echo ${q(remoteSettingsJson)} > ${remoteWorktreeDir}/.gemini/settings.json`)}`);
// 4. Execution Logic
// In shell mode, we just start gemini. In action mode, we run the entrypoint.
const remoteWorker = isShellMode
? `gemini`
: `tsx ${persistentScripts}/entrypoint.ts ${prNumber} . ${remotePolicyPath} ${action}`;
const authEnv = `-e GEMINI_AUTO_UPDATE=0 ${remoteApiKey ? `-e GEMINI_API_KEY=${remoteApiKey} ` : ''}${remoteGhToken ? `-e GITHUB_TOKEN=${remoteGhToken} -e GH_TOKEN=${remoteGhToken} ` : ''}`;
// PERSISTENCE: Wrap the entire execution in a tmux session inside the container
// We HIDE the tmux status bar to reduce visual noise
const tmuxStyle = `
tmux set -g status off;
`.replace(/\n/g, '');
const tmuxCmd = `tmux new-session -A -s ${sessionName} ${q(`${tmuxStyle} cd ${remoteWorktreeDir} && ${remoteWorker}; exec $SHELL`)}`;
const containerWrap = `sudo docker exec -it -e COLORTERM=truecolor -e TERM=xterm-256color ${authEnv}maintainer-worker sh -c ${q(tmuxCmd)}`;
const finalSSH = provider.getRunCommand(containerWrap, { interactive: true });
const isWithinGemini = !!env.GEMINI_CLI || !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID;
// 1.5 Handle --open override
const openIdx = args.indexOf('--open');
let terminalTarget = config.terminalTarget || 'tab';
if (openIdx !== -1 && args[openIdx + 1]) {
terminalTarget = args[openIdx + 1];
}
const forceMainTerminal = terminalTarget === 'foreground';
if (!forceMainTerminal && isWithinGemini && env.TERM_PROGRAM === 'iTerm.app') {
const tempCmdPath = path.join(process.env.TMPDIR || '/tmp', `workspace-ssh-${prNumber}.sh`);
fs.writeFileSync(tempCmdPath, `#!/bin/bash\n${finalSSH}\nrm "$0"`, { mode: 0o755 });
const appleScript = terminalTarget === 'window' ? `
on run argv
tell application "iTerm"
set newWindow to (create window with default profile)
tell current session of newWindow
write text (quoted form of item 1 of argv) & return
end tell
activate
end tell
end run
` : `
on run argv
tell application "iTerm"
tell current window
set newTab to (create tab with default profile)
tell current session of newTab
write text (quoted form of item 1 of argv) & return
end tell
end tell
activate
end tell
end run
`;
spawnSync('osascript', ['-', tempCmdPath], { input: appleScript });
console.log(`✅ iTerm2 ${terminalTarget} opened for job ${prNumber}.`);
return 0;
}
// Fallback: Run in current terminal
console.log(`📡 Connecting to session ${sessionName}...`);
spawnSync(finalSSH, { stdio: 'inherit', shell: true });
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runOrchestrator(process.argv.slice(2)).catch(console.error);
}
@@ -0,0 +1,18 @@
import { spawnSync } from 'child_process';
import path from 'path';
export async function runFixPlaybook(prNumber: string, targetDir: string, policyPath: string, geminiBin: string) {
console.log(`🚀 Workspace | FIX | PR #${prNumber}`);
console.log('Switching to agentic fix loop inside Gemini CLI...');
// Use the nightly gemini binary to activate the fix-pr skill and iterate
// Note: Gemini doesn't support --cwd, so the caller (worker.ts) must ensure we are already in targetDir
const result = spawnSync(geminiBin, [
'--policy', policyPath,
'-p', `Please activate the 'fix-pr' skill and use it to iteratively fix PR #${prNumber}.
Ensure you handle CI failures, merge conflicts, and unaddressed review comments
until the PR is fully passing and mergeable.`
], { stdio: 'inherit' });
return result?.status ?? 1;
}
@@ -0,0 +1,74 @@
import { TaskRunner } from '../TaskRunner.js';
import path from 'path';
import { spawnSync } from 'child_process';
import { TaskRunner } from '../TaskRunner.js';
import path from 'path';
import { spawnSync } from 'child_process';
import fs from 'fs';
export async function runImplementPlaybook(issueNumber: string, workDir: string, policyPath: string, geminiBin: string) {
console.log(`🚀 Workspace | IMPLEMENT (Supervisor Loop) | Issue #${issueNumber}`);
const ghView = spawnSync('gh', ['issue', 'view', issueNumber, '--json', 'title,body', '-q', '{title:.title,body:.body}'], { shell: true });
const meta = JSON.parse(ghView.stdout.toString());
const branchName = `impl/${issueNumber}-${meta.title.toLowerCase().replace(/[^a-z0-9]/g, '-')}`.slice(0, 50);
// 1. Initial Research & Test Creation
console.log('\n🧠 Phase 1: Research & Reproduction...');
spawnSync(geminiBin, [
'--policy', policyPath, '--cwd', workDir,
'-p', `Research Issue #${issueNumber}: "${meta.title}".
Description: ${meta.body}.
ACTION: Create a NEW Vitest test file in 'tests/repro_issue_${issueNumber}.test.ts' that demonstrates the issue or feature.
Ensure this test fails currently.`
], { stdio: 'inherit' });
// 2. The Self-Healing Loop
let attempts = 0;
const maxAttempts = 5;
let success = false;
console.log('\n🛠️ Phase 2: Implementation Loop...');
while (attempts < maxAttempts && !success) {
attempts++;
console.log(`\n👉 Attempt ${attempts}/${maxAttempts}...`);
// Run the specific repro test
const testRun = spawnSync('npx', ['vitest', 'run', `tests/repro_issue_${issueNumber}.test.ts`], { cwd: workDir });
if (testRun.status === 0) {
console.log('✅ Reproduction test PASSED!');
success = true;
break;
}
console.log('❌ Test failed. Asking Gemini to fix the implementation...');
const testError = testRun.stdout.toString() + testRun.stderr.toString();
spawnSync(geminiBin, [
'--policy', policyPath, '--cwd', workDir,
'-p', `The reproduction test for Issue #${issueNumber} is still failing.
ERROR OUTPUT:
${testError.slice(-2000)}
ACTION: Modify the source code to fix this error and make the test pass.
Do not modify the test itself unless it has a syntax error.`
], { stdio: 'inherit' });
}
// 3. Final Verification
if (success) {
console.log('\n🧪 Phase 3: Final Verification...');
const finalCheck = spawnSync('npm', ['test'], { cwd: workDir, stdio: 'inherit' });
if (finalCheck.status === 0) {
console.log('\n🎉 Implementation complete and verified!');
spawnSync('git', ['add', '.'], { cwd: workDir });
spawnSync('git', ['commit', '-m', `feat: implement issue #${issueNumber}`], { cwd: workDir });
return 0;
}
}
console.error('\n❌ Supervisor: Failed to reach a passing state within retry limit.');
return 1;
}
@@ -0,0 +1,17 @@
import { TaskRunner } from '../TaskRunner.ts';
import path from 'path';
export async function runReadyPlaybook(prNumber: string, targetDir: string, policyPath: string, geminiBin: string) {
const runner = new TaskRunner(
path.join(targetDir, `.gemini/logs/workspace-${prNumber}`),
`🚀 Workspace | READY | PR #${prNumber}`
);
runner.register([
{ id: 'clean', name: 'Clean Workspace', cmd: `npm run clean && npm ci` },
{ id: 'preflight', name: 'Full Preflight', cmd: `npm run preflight`, dep: 'clean' },
{ id: 'conflicts', name: 'Main Conflict Check', cmd: `git fetch origin main && git merge-base --is-ancestor origin/main HEAD` }
]);
return runner.run();
}
@@ -0,0 +1,17 @@
import { TaskRunner } from '../TaskRunner.ts';
import path from 'path';
export async function runReviewPlaybook(prNumber: string, targetDir: string, policyPath: string, geminiBin: string) {
const runner = new TaskRunner(
path.join(targetDir, `.gemini/logs/workspace-${prNumber}`),
`🚀 Workspace | REVIEW | PR #${prNumber}`
);
runner.register([
{ id: 'build', name: 'Fast Build', cmd: `cd ${targetDir} && npm ci && npm run build` },
{ id: 'ci', name: 'CI Checks', cmd: `gh pr checks ${prNumber}` },
{ id: 'review', name: 'Workspaceed Review', cmd: `cd ${targetDir} && ${geminiBin} --policy ${policyPath} -p "Please activate the 'review-pr' skill and use it to conduct a behavioral review of PR #${prNumber}."` }
]);
return runner.run();
}
@@ -0,0 +1,82 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* WorkspaceProvider interface defines the contract for different remote
* execution environments (GCE, Workstations, etc.).
*/
export interface WorkspaceProvider {
/**
* Provisions the underlying infrastructure.
*/
provision(): Promise<number>;
/**
* Ensures the workspace is running and accessible.
*/
ensureReady(): Promise<number>;
/**
* Performs the initial setup of the workspace (SSH, scripts, auth).
*/
setup(options: SetupOptions): Promise<number>;
/**
* Returns the raw command string that would be used to execute a command.
*/
getRunCommand(command: string, options?: ExecOptions): string;
/**
* Executes a command on the workspace.
*/
exec(command: string, options?: ExecOptions): Promise<number>;
/**
* Executes a command on the workspace and returns the output.
*/
getExecOutput(command: string, options?: ExecOptions): Promise<{ status: number; stdout: string; stderr: string }>;
/**
* Synchronizes local files to the workspace.
*/
sync(localPath: string, remotePath: string, options?: SyncOptions): Promise<number>;
/**
* Returns the status of the workspace.
*/
getStatus(): Promise<WorkspaceStatus>;
/**
* Stops the workspace to save costs.
*/
stop(): Promise<number>;
}
export interface SetupOptions {
projectId: string;
zone: string;
dnsSuffix?: string;
syncAuth?: boolean;
}
export interface ExecOptions {
interactive?: boolean;
cwd?: string;
wrapContainer?: string;
}
export interface SyncOptions {
delete?: boolean;
exclude?: string[];
sudo?: boolean;
}
export interface WorkspaceStatus {
name: string;
status: string;
internalIp?: string;
externalIp?: string;
}
@@ -0,0 +1,83 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'child_process';
import os from 'os';
/**
* Centralized SSH/RSYNC management for GCE Workers.
* Handles Magic Hostname routing with Zero-Knowledge security.
* STRICTLY uses Direct Internal connection (Corporate Magic).
*/
export class GceConnectionManager {
private projectId: string;
private zone: string;
private instanceName: string;
constructor(projectId: string, zone: string, instanceName: string) {
this.projectId = projectId;
this.zone = zone;
this.instanceName = instanceName;
}
getMagicRemote(): string {
const user = `${process.env.USER || 'node'}_google_com`;
const dnsSuffix = '.internal.gcpnode.com';
return `${user}@nic0.${this.instanceName}.${this.zone}.c.${this.projectId}${dnsSuffix}`;
}
getCommonArgs(): string[] {
return [
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'LogLevel=ERROR',
'-o', 'ConnectTimeout=60',
'-o', 'ServerAliveInterval=30',
'-o', 'ServerAliveCountMax=3',
'-o', 'SendEnv=USER',
'-i', `${os.homedir()}/.ssh/google_compute_engine`
];
}
getRunCommand(command: string, options: { interactive?: boolean } = {}): string {
const fullRemote = this.getMagicRemote();
return `ssh ${this.getCommonArgs().join(' ')} ${options.interactive ? '-t' : ''} ${fullRemote} ${this.quote(command)}`;
}
run(command: string, options: { interactive?: boolean; stdio?: 'pipe' | 'inherit' } = {}): { status: number; stdout: string; stderr: string } {
const sshCmd = this.getRunCommand(command, options);
const res = spawnSync(sshCmd, { stdio: options.stdio || 'pipe', shell: true });
return {
status: res.status ?? 1,
stdout: res.stdout?.toString() || '',
stderr: res.stderr?.toString() || ''
};
}
sync(localPath: string, remotePath: string, options: { delete?: boolean; exclude?: string[]; sudo?: boolean } = {}): number {
const fullRemote = this.getMagicRemote();
// We use --no-t and --no-perms to avoid "Operation not permitted" errors
// when syncing to volumes that might have UID mismatches with the container.
const rsyncArgs = ['-rvz', '--quiet', '--no-t', '--no-perms', '--no-owner', '--no-group'];
if (options.delete) rsyncArgs.push('--delete');
if (options.exclude) options.exclude.forEach(ex => rsyncArgs.push(`--exclude="${ex}"`));
// Use sudo on the remote side if requested to bypass permission errors
if (options.sudo) {
rsyncArgs.push('--rsync-path="sudo rsync"');
}
const sshCmd = `ssh ${this.getCommonArgs().join(' ')}`;
const directRsync = `rsync ${rsyncArgs.join(' ')} -e ${this.quote(sshCmd)} ${localPath} ${fullRemote}:${remotePath}`;
const res = spawnSync(directRsync, { stdio: 'inherit', shell: true });
return res.status ?? 1;
}
private quote(str: string) {
return `'${str.replace(/'/g, "'\\''")}'`;
}
}
@@ -0,0 +1,304 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { WorkspaceProvider, SetupOptions, ExecOptions, SyncOptions, WorkspaceStatus } from './BaseProvider.ts';
import { GceConnectionManager } from './GceConnectionManager.ts';
export class GceCosProvider implements WorkspaceProvider {
private projectId: string;
private zone: string;
private instanceName: string;
private sshConfigPath: string;
private knownHostsPath: string;
private sshAlias = 'gcli-worker';
private conn: GceConnectionManager;
constructor(projectId: string, zone: string, instanceName: string, repoRoot: string) {
this.projectId = projectId;
this.zone = zone;
this.instanceName = instanceName;
const workspacesDir = path.join(repoRoot, '.gemini/workspaces');
if (!fs.existsSync(workspacesDir)) fs.mkdirSync(workspacesDir, { recursive: true });
this.sshConfigPath = path.join(workspacesDir, 'ssh_config');
this.knownHostsPath = path.join(workspacesDir, 'known_hosts');
this.conn = new GceConnectionManager(projectId, zone, instanceName);
}
async provision(): Promise<number> {
const imageUri = 'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest';
const region = this.zone.split('-').slice(0, 2).join('-');
const vpcName = 'iap-vpc';
const subnetName = 'iap-subnet';
console.log(`🏗️ Ensuring "Magic" Network Infrastructure in ${this.projectId}...`);
const vpcCheck = spawnSync('gcloud', ['compute', 'networks', 'describe', vpcName, '--project', this.projectId], { stdio: 'pipe' });
if (vpcCheck.status !== 0) {
spawnSync('gcloud', ['compute', 'networks', 'create', vpcName, '--project', this.projectId, '--subnet-mode=custom'], { stdio: 'inherit' });
}
const subnetCheck = spawnSync('gcloud', ['compute', 'networks', 'subnets', 'describe', subnetName, '--project', this.projectId, '--region', region], { stdio: 'pipe' });
if (subnetCheck.status !== 0) {
spawnSync('gcloud', ['compute', 'networks', 'subnets', 'create', subnetName,
'--project', this.projectId, '--network', vpcName, '--region', region,
'--range=10.0.0.0/24', '--enable-private-ip-google-access'], { stdio: 'inherit' });
} else {
spawnSync('gcloud', ['compute', 'networks', 'subnets', 'update', subnetName, '--project', this.projectId, '--region', region, '--enable-private-ip-google-access'], { stdio: 'pipe' });
}
const fwCheck = spawnSync('gcloud', ['compute', 'firewall-rules', 'describe', 'allow-corporate-ssh', '--project', this.projectId], { stdio: 'pipe' });
if (fwCheck.status !== 0) {
spawnSync('gcloud', ['compute', 'firewall-rules', 'create', 'allow-corporate-ssh',
'--project', this.projectId, '--network', vpcName, '--allow=tcp:22', '--source-ranges=0.0.0.0/0'], { stdio: 'inherit' });
}
console.log(`🚀 Provisioning GCE COS worker: ${this.instanceName} (Unified Workspace Setup)...`);
const startupScriptContent = `#!/bin/bash
set -e
echo "🚀 Initializing Unified Workspace..."
# 1. Mount Data Disk
mkdir -p /mnt/disks/data
if ! mountpoint -q /mnt/disks/data; then
DATA_DISK="/dev/disk/by-id/google-data"
[ -e "$DATA_DISK" ] || DATA_DISK="/dev/sdb"
while [ ! -e "$DATA_DISK" ]; do echo "Waiting for data disk..."; sleep 1; done
blkid "$DATA_DISK" || mkfs.ext4 -m 0 -F "$DATA_DISK"
mount -o discard,defaults "$DATA_DISK" /mnt/disks/data
fi
# 2. Prepare Stateful Directories (on the persistent disk)
mkdir -p /mnt/disks/data/main /mnt/disks/data/worktrees /mnt/disks/data/scripts /mnt/disks/data/config /mnt/disks/data/policies
chmod -R 777 /mnt/disks/data
# 3. Handle Unified Path Symlink (/home/node/.workspaces)
# This ensures absolute paths match perfectly between host and container.
mkdir -p /home/node
ln -sfn /mnt/disks/data /home/node/.workspaces
chown -R 1000:1000 /home/node
# Also ensure host users can find it
ln -sfn /mnt/disks/data /workspaces
chmod 777 /workspaces
for h in /home/*_google_com; do
[ -d "$h" ] || continue
ln -sfn /mnt/disks/data "$h/.workspaces"
chown -h $(basename $h):$(basename $h) "$h/.workspaces"
done
# 4. Container Resilience Loop
until docker info >/dev/null 2>&1; do echo "Waiting for docker..."; sleep 2; done
for i in {1..5}; do
docker pull ${imageUri} && break || (echo "Pull failed, retry $i..." && sleep 5)
done
if ! docker ps -a | grep -q "maintainer-worker"; then
docker run -d --name maintainer-worker --restart always \\
-v /mnt/disks/data:/home/node/.workspaces:rw \\
-v /mnt/disks/data/gemini-cli-config/.gemini:/home/node/.gemini:rw \\
-v ~/.config/gh:/home/node/.config/gh:rw \\
${imageUri} /bin/bash -c "while true; do sleep 1000; done"
fi
echo "✅ Unified Workspace is active."
`;
const tmpScriptPath = path.join(os.tmpdir(), `gcli-startup-${Date.now()}.sh`);
fs.writeFileSync(tmpScriptPath, startupScriptContent);
const result = spawnSync('gcloud', [
'compute', 'instances', 'create', this.instanceName,
'--project', this.projectId,
'--zone', this.zone,
'--machine-type', 'n2-standard-8',
'--image-family', 'cos-stable',
'--image-project', 'cos-cloud',
'--boot-disk-size', '10GB',
'--boot-disk-type', 'pd-balanced',
'--create-disk', `name=${this.instanceName}-data,size=200,type=pd-balanced,device-name=data,auto-delete=yes`,
'--metadata-from-file', `startup-script=${tmpScriptPath}`,
'--metadata', 'enable-oslogin=TRUE',
'--network-interface', `network=${vpcName},subnet=${subnetName},no-address`,
'--scopes', 'https://www.googleapis.com/auth/cloud-platform',
'--quiet'
], { stdio: 'inherit' });
fs.unlinkSync(tmpScriptPath);
if (result.status === 0) {
console.log('⏳ Waiting for OS Login and SSH to initialize (this takes ~45s)...');
await new Promise(r => setTimeout(r, 45000));
}
return result.status ?? 1;
}
async ensureReady(): Promise<number> {
const status = await this.getStatus();
if (status.status !== 'RUNNING') {
console.log(`⚠️ Worker ${this.instanceName} is ${status.status}. Waking it up...`);
const res = spawnSync('gcloud', [
'compute', 'instances', 'start', this.instanceName,
'--project', this.projectId,
'--zone', this.zone
], { stdio: 'inherit' });
if (res.status !== 0) return res.status ?? 1;
console.log('⏳ Waiting for boot...');
await new Promise(r => setTimeout(r, 20000));
}
// NEW: Verify the container is actually running AND up to date
console.log(' - Verifying remote container health and image version...');
const containerCheck = await this.getExecOutput('sudo docker ps -q --filter "name=maintainer-worker"');
let needsUpdate = false;
if (containerCheck.status === 0 && containerCheck.stdout.trim()) {
// Check if the volume mounts are correct by checking for files inside .workspaces/main
const mountCheck = await this.getExecOutput('sudo docker exec maintainer-worker ls -A /home/node/.workspaces/main');
if (mountCheck.status !== 0 || !mountCheck.stdout.trim()) {
console.log(' ⚠️ Remote container has incorrect or empty mounts. Triggering refresh...');
needsUpdate = true;
} else {
// Check if the running image is stale
const imageUri = 'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest';
const tmuxCheck = await this.getExecOutput('sudo docker exec maintainer-worker which tmux');
if (tmuxCheck.status !== 0) {
console.log(' ⚠️ Remote container is stale (missing tmux). Triggering update...');
needsUpdate = true;
}
}
} else {
needsUpdate = true;
}
if (needsUpdate) {
console.log(' ⚠️ Container missing or stale. Attempting refresh...');
const imageUri = 'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest';
// Ensure data mount is available before running
const recoverCmd = `
(mountpoint -q /mnt/disks/data || sudo mount /dev/disk/by-id/google-data /mnt/disks/data) && \
sudo docker pull ${imageUri} && \
(sudo docker rm -f maintainer-worker || true) && \
sudo docker run -d --name maintainer-worker --restart always \
-v /mnt/disks/data:/home/node/.workspaces:rw \
-v /mnt/disks/data/gemini-cli-config/.gemini:/home/node/.gemini:rw \
-v ~/.config/gh:/home/node/.config/gh:rw \
${imageUri} /bin/bash -c "while true; do sleep 1000; done"
`;
const recoverRes = await this.exec(recoverCmd);
if (recoverRes !== 0) {
console.error(' ❌ Critical: Failed to refresh maintainer container.');
return 1;
}
console.log(' ✅ Container refreshed.');
}
return 0;
}
async setup(options: SetupOptions): Promise<number> {
const dnsSuffix = options.dnsSuffix || '.internal.gcpnode.com';
const internalHostname = `nic0.${this.instanceName}.${this.zone}.c.${this.projectId}${dnsSuffix.startsWith('.') ? dnsSuffix : '.' + dnsSuffix}`;
const user = `${process.env.USER || 'node'}_google_com`;
const sshEntry = `
Host ${this.sshAlias}
HostName ${internalHostname}
IdentityFile ~/.ssh/google_compute_engine
User ${user}
UserKnownHostsFile /dev/null
CheckHostIP no
StrictHostKeyChecking no
ConnectTimeout 60
ServerAliveInterval 30
`;
fs.writeFileSync(this.sshConfigPath, sshEntry);
console.log(` ✅ Created project SSH config: ${this.sshConfigPath}`);
console.log(' - Verifying direct connection (may trigger corporate SSO prompt)...');
const res = this.conn.run('echo 1');
if (res.status !== 0) {
console.error('\n❌ All connection attempts failed. Please ensure you have "gcert" and IAP permissions.');
return 1;
}
console.log(' ✅ Connection verified. Waiting 10s for remote disk initialization...');
await new Promise(r => setTimeout(r, 10000));
return 0;
}
getRunCommand(command: string, options: ExecOptions = {}): string {
let finalCmd = command;
if (options.wrapContainer) {
finalCmd = `sudo docker exec ${options.interactive ? '-it' : ''} ${options.cwd ? `-w ${options.cwd}` : ''} ${options.wrapContainer} sh -c ${this.quote(command)}`;
}
return this.conn.getRunCommand(finalCmd, { interactive: options.interactive });
}
async exec(command: string, options: ExecOptions = {}): Promise<number> {
const res = await this.getExecOutput(command, options);
return res.status;
}
async getExecOutput(command: string, options: ExecOptions = {}): Promise<{ status: number; stdout: string; stderr: string }> {
let finalCmd = command;
if (options.wrapContainer) {
finalCmd = `sudo docker exec ${options.interactive ? '-it' : ''} ${options.cwd ? `-w ${options.cwd}` : ''} ${options.wrapContainer} sh -c ${this.quote(command)}`;
}
return this.conn.run(finalCmd, { interactive: options.interactive, stdio: options.interactive ? 'inherit' : 'pipe' });
}
async sync(localPath: string, remotePath: string, options: SyncOptions = {}): Promise<number> {
console.log(`📦 Syncing ${localPath} to remote:${remotePath}...`);
return this.conn.sync(localPath, remotePath, options);
}
async getStatus(): Promise<WorkspaceStatus> {
const res = spawnSync('gcloud', [
'compute', 'instances', 'describe', this.instanceName,
'--project', this.projectId,
'--zone', this.zone,
'--format', 'json(name,status,networkInterfaces[0].networkIP)'
], { stdio: 'pipe' });
if (res.status !== 0) {
return { name: this.instanceName, status: 'UNKNOWN' };
}
try {
const data = JSON.parse(res.stdout.toString());
return {
name: data.name,
status: data.status,
internalIp: data.networkInterfaces?.[0]?.networkIP
};
} catch (e) {
return { name: this.instanceName, status: 'ERROR' };
}
}
async stop(): Promise<number> {
const res = spawnSync('gcloud', [
'compute', 'instances', 'stop', this.instanceName,
'--project', this.projectId,
'--zone', this.zone
], { stdio: 'inherit' });
return res.status ?? 1;
}
private quote(str: string) {
return `'${str.replace(/'/g, "'\\''")}'`;
}
}
@@ -0,0 +1,20 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { GceCosProvider } from './GceCosProvider.ts';
import { WorkspaceProvider } from './BaseProvider.ts';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../../..');
export class ProviderFactory {
static getProvider(config: { projectId: string; zone: string; instanceName: string }): WorkspaceProvider {
// Currently we only have GceCosProvider, but this is where we'd branch
return new GceCosProvider(config.projectId, config.zone, config.instanceName, REPO_ROOT);
}
}
@@ -0,0 +1,59 @@
#!/bin/bash
set -e
# Ensure we have a valid environment for non-interactive startup
export USER=${USER:-ubuntu}
export HOME=/home/$USER
export DEBIAN_FRONTEND=noninteractive
echo "🛠️ Provisioning High-Performance Gemini CLI Maintainer Worker..."
# Wait for apt lock
wait_for_apt() {
while sudo fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock >/dev/null 2>&1 ; do
sleep 2
done
}
wait_for_apt
# 1. System Essentials (Inc. libraries for native node modules)
apt-get update && apt-get install -y \
curl git git-lfs tmux build-essential unzip jq gnupg cron \
libsecret-1-dev libkrb5-dev
# 2. GitHub CLI
if ! command -v gh &> /dev/null; then
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null
wait_for_apt
apt-get update && apt-get install gh -y
fi
# 3. Direct Node.js 20 Installation (NodeSource)
if ! command -v node &> /dev/null; then
echo "Installing Node.js 20..."
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
wait_for_apt
apt-get install -y nodejs
fi
# 4. Global Maintenance Tooling
echo "Installing global developer tools..."
npm install -g tsx vitest @google/gemini-cli@nightly
# 5. Pre-warm Repository (Main Hub)
# We clone and build the main repo in the image so that new worktrees start with a warm cache
REMOTE_WORK_DIR="$HOME/dev/main"
mkdir -p "$HOME/dev"
if [ ! -d "$REMOTE_WORK_DIR" ]; then
echo "Pre-cloning and building repository..."
git clone --filter=blob:none https://github.com/google-gemini/gemini-cli.git "$REMOTE_WORK_DIR"
cd "$REMOTE_WORK_DIR"
npm install --no-audit --no-fund
npm run build
fi
chown -R $USER:$USER $HOME/dev
echo "✅ Provisioning Complete!"
+383
View File
@@ -0,0 +1,383 @@
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { fileURLToPath } from 'url';
import readline from 'readline';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = process.cwd();
/**
* Loads and parses a local .env file from the repository root and the home directory.
*/
function loadDotEnv() {
const envPaths = [
path.join(REPO_ROOT, '.env'),
path.join(os.homedir(), '.env')
];
envPaths.forEach(envPath => {
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, 'utf8');
content.split('\n').forEach(line => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) return;
const match = trimmed.match(/^([^=]+)=(.*)$/);
if (match) {
const key = match[1].trim();
const val = match[2].trim().replace(/^["'](.*)["']$/, '$1');
if (!process.env[key]) process.env[key] = val;
}
});
}
});
}
async function prompt(question: string, defaultValue: string, explanation?: string, sensitive: boolean = false): Promise<string> {
const autoAccept = process.argv.includes('--yes') || process.argv.includes('-y');
if (autoAccept && defaultValue) return defaultValue;
if (explanation) {
console.log(`\n📖 ${explanation}`);
}
const displayDefault = sensitive && defaultValue ? `${defaultValue.substring(0, 4)}...${defaultValue.substring(defaultValue.length - 4)}` : defaultValue;
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const promptMsg = defaultValue
? `${question} [Detected: ${displayDefault}] (Press <Enter> to keep, or type new value): `
: `${question} (<Enter> for none): `;
return new Promise((resolve) => {
rl.question(promptMsg, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue);
});
});
}
async function confirm(question: string): Promise<boolean> {
const autoAccept = process.argv.includes('--yes') || process.argv.includes('-y');
if (autoAccept) return true;
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${question} (y/n): `, (answer) => {
rl.close();
resolve(answer.trim().toLowerCase() === 'y');
});
});
}
async function createFork(upstream: string): Promise<string> {
console.log(` - Creating fork for ${upstream}...`);
const forkRes = spawnSync('gh', ['repo', 'fork', upstream, '--clone=false'], { stdio: 'inherit' });
if (forkRes.status === 0) {
const userRes = spawnSync('gh', ['api', 'user', '-q', '.login'], { stdio: 'pipe' });
const user = userRes.stdout.toString().trim();
return `${user}/${upstream.split('/')[1]}`;
}
return upstream;
}
export async function runSetup(env: NodeJS.ProcessEnv = process.env) {
loadDotEnv();
console.log(`
================================================================================
🚀 GEMINI WORKSPACES: HIGH-PERFORMANCE REMOTE DEVELOPMENT
================================================================================
Workspaces allow you to delegate heavy tasks (PR reviews, agentic fixes,
and full builds) to a dedicated, high-performance GCP worker.
================================================================================
`);
console.log('📝 PHASE 1: CONFIGURATION');
console.log('--------------------------------------------------------------------------------');
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
let settings: any = {};
let skipConfig = false;
if (fs.existsSync(settingsPath)) {
try {
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
if (settings.workspace && !process.argv.includes('--reconfigure')) {
console.log(' ✅ Existing configuration found.');
const shouldSkip = await confirm('Use existing configuration and skip to execution?');
if (shouldSkip) {
skipConfig = true;
}
}
} catch (e) {}
}
// 1. Project Identity
let projectId = settings.workspace?.projectId || '';
let zone = settings.workspace?.zone || 'us-west1-a';
let terminalTarget = settings.workspace?.terminalTarget || 'tab';
let upstreamRepo = settings.workspace?.upstreamRepo || 'google-gemini/gemini-cli';
let userFork = settings.workspace?.userFork || upstreamRepo;
if (!skipConfig) {
const defaultProject = env.GOOGLE_CLOUD_PROJECT || env.WORKSPACE_PROJECT || projectId || '';
projectId = await prompt('GCP Project ID', defaultProject,
'The GCP Project where your workspace worker will live. Your personal project is recommended.');
if (!projectId) {
console.error('❌ Project ID is required. Set GOOGLE_CLOUD_PROJECT or enter it manually.');
return 1;
}
zone = await prompt('Compute Zone', env.WORKSPACE_ZONE || zone,
'The physical location of your worker. us-west1-a is the team default.');
terminalTarget = await prompt('Terminal UI Target (foreground, background, tab, window)', env.WORKSPACE_TERM_TARGET || terminalTarget,
'When you start a job in gemini-cli, should it run as a foreground shell, background shell (no attach), new iterm2 tab, or new iterm2 window?');
// 2. Repository Discovery (Dynamic)
console.log('\n🔍 Detecting repository origins...');
const repoInfoRes = spawnSync('gh', ['repo', 'view', '--json', 'nameWithOwner,parent,isFork'], { stdio: 'pipe' });
if (repoInfoRes.status === 0) {
try {
const repoInfo = JSON.parse(repoInfoRes.stdout.toString());
upstreamRepo = repoInfo.isFork && repoInfo.parent ? repoInfo.parent.nameWithOwner : repoInfo.nameWithOwner;
console.log(` - Upstream identified: ${upstreamRepo}`);
console.log(` - Searching for your forks of ${upstreamRepo}...`);
const upstreamOwner = upstreamRepo.split('/')[0];
const upstreamName = upstreamRepo.split('/')[1];
const gqlQuery = `query { viewer { repositories(first: 100, isFork: true, affiliations: OWNER) { nodes { nameWithOwner parent { nameWithOwner } } } } }`;
const forksRes = spawnSync('gh', ['api', 'graphql', '-f', `query=${gqlQuery}`, '--jq', `.data.viewer.repositories.nodes[] | select(.parent.nameWithOwner == "${upstreamRepo}") | .nameWithOwner`], { stdio: 'pipe' });
const myForks = forksRes.stdout.toString().trim().split('\n').filter(Boolean);
if (myForks.length > 0) {
console.log('\n🍴 Found existing forks:');
myForks.forEach((name: string, i: number) => console.log(` [${i + 1}] ${name}`));
console.log(` [c] Create a new fork`);
console.log(` [u] Use upstream directly (not recommended)`);
const choice = await prompt('Select an option', '1');
if (choice.toLowerCase() === 'c') {
userFork = await createFork(upstreamRepo);
} else if (choice.toLowerCase() === 'u') {
userFork = upstreamRepo;
} else {
const idx = parseInt(choice) - 1;
userFork = myForks[idx] || myForks[0];
}
} else {
const shouldFork = await confirm('No fork detected. Create a personal fork for sandboxed implementations?');
userFork = shouldFork ? await createFork(upstreamRepo) : upstreamRepo;
}
} catch (e) {
userFork = upstreamRepo;
}
}
console.log(` ✅ Upstream: ${upstreamRepo}`);
console.log(` ✅ Workspace: ${userFork}`);
}
// 3. Security & Auth (Always check for token if init is needed)
let githubToken = env.WORKSPACE_GH_TOKEN || '';
if (!skipConfig) {
if (!githubToken) {
const hasToken = await confirm('\nDo you already have a GitHub Personal Access Token (PAT) with "Read/Write" access to contents & PRs?');
if (hasToken) {
githubToken = await prompt('Paste Scoped Token', '');
} else {
const shouldGenToken = await confirm('Would you like to generate a new scoped token now? (Highly Recommended)');
if (shouldGenToken) {
const baseUrl = 'https://github.com/settings/personal-access-tokens/new';
const name = `Workspace-${env.USER}`;
const repoParams = userFork !== upstreamRepo
? `&repositories[]=${encodeURIComponent(upstreamRepo)}&repositories[]=${encodeURIComponent(userFork)}`
: `&repositories[]=${encodeURIComponent(upstreamRepo)}`;
const magicLink = `${baseUrl}?name=${encodeURIComponent(name)}&description=Gemini+Workspaces+Worker${repoParams}&contents=write&pull_requests=write&metadata=read`;
const terminalLink = `\u001b]8;;${magicLink}\u0007${magicLink}\u001b]8;;\u0007`;
console.log(`\n🔐 ACTION REQUIRED: Create a token with the required permissions:`);
console.log(`\n${terminalLink}\n`);
githubToken = await prompt('Paste Scoped Token', '');
}
}
} else {
githubToken = await prompt('GitHub Token', githubToken, 'A GitHub PAT is required for remote repository access and PR operations.', true);
}
}
// 4. Gemini API Auth Strategy
console.log('\n🔐 Detecting Gemini Authentication strategy...');
const localSettingsPath = path.join(env.HOME || '', '.gemini/settings.json');
let authStrategy = 'google_accounts';
let geminiApiKey = env.WORKSPACE_GEMINI_API_KEY || env.GEMINI_API_KEY || '';
if (fs.existsSync(localSettingsPath)) {
try {
const localSettings = JSON.parse(fs.readFileSync(localSettingsPath, 'utf8'));
authStrategy = localSettings.security?.auth?.selectedType || 'google_accounts';
if (!geminiApiKey && localSettings.security?.auth?.apiKey) {
geminiApiKey = localSettings.security.auth.apiKey;
}
console.log(` - Local Auth Method: ${authStrategy}`);
} catch (e) {}
}
if (authStrategy === 'gemini-api-key') {
if (geminiApiKey) {
console.log('\n🔐 Found Gemini API Key in environment or settings.');
geminiApiKey = await prompt('Gemini API Key', geminiApiKey, 'Enter to use? Or paste a new one', true);
} else {
console.log('\n📖 In API Key mode, the remote worker needs your Gemini API Key to authenticate.');
geminiApiKey = await prompt('Gemini API Key', '', 'Paste your Gemini API Key', true);
}
} else {
console.log(` - Using current auth strategy: ${authStrategy}`);
}
// 5. Save Confirmed State
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
if (!fs.existsSync(path.dirname(settingsPath))) fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
settings = {
workspace: {
projectId, zone, terminalTarget,
userFork, upstreamRepo,
remoteHost: 'gcli-worker',
remoteWorkDir: '~/dev/main',
useContainer: true
}
};
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
console.log(`\n✅ Configuration saved to ${settingsPath}`);
// Transition to Execution
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
console.log('\n🏗️ PHASE 2: INFRASTRUCTURE');
console.log('--------------------------------------------------------------------------------');
console.log(` - Verifying access and finding worker ${targetVM}...`);
let status = await provider.getStatus();
if (status.status === 'UNKNOWN' || status.status === 'ERROR') {
const shouldProvision = await confirm(`Worker ${targetVM} not found. Provision it now?`);
if (!shouldProvision) return 1;
const provisionRes = await provider.provision();
if (provisionRes !== 0) return 1;
status = await provider.getStatus();
}
if (status.status !== 'RUNNING') {
console.log(' - Waking up worker...');
await provider.ensureReady();
}
console.log('\n🚀 PHASE 3: REMOTE INITIALIZATION');
console.log('--------------------------------------------------------------------------------');
const setupRes = await provider.setup({ projectId, zone, dnsSuffix: '.internal.gcpnode.com' });
if (setupRes !== 0) return setupRes;
// Use the unified path to ensure host and container match perfectly
const workspaceRoot = `/home/node/.workspaces`;
const persistentScripts = `${workspaceRoot}/scripts`;
const remoteConfigDir = `${workspaceRoot}/gemini-cli-config/.gemini`;
console.log(`\n📦 Synchronizing Logic & Credentials...`);
// Ensure the directory structure exists on the host
await provider.exec(`sudo mkdir -p ${workspaceRoot}/main ${workspaceRoot}/worktrees ${workspaceRoot}/policies ${workspaceRoot}/scripts ${remoteConfigDir}`);
await provider.exec(`sudo chown -R 1000:1000 ${workspaceRoot}`);
await provider.exec(`sudo chmod -R 777 ${workspaceRoot}`);
// 1. Sync Scripts & Policies
await provider.sync('extensions/workspaces/scripts/', `${persistentScripts}/`, { delete: true, sudo: true });
await provider.sync('policies/workspaces/workspace-policy.toml', `${workspaceRoot}/policies/workspace-policy.toml`, { sudo: true });
// 2. Initialize Remote Gemini Config with Auth
console.log('⚙️ Initializing remote Gemini configuration...');
// NEW: Sync local theme and UI preferences
let localTheme = 'Shades Of Purple';
let useAlternateBuffer = true;
let useBackgroundColor = true;
if (fs.existsSync(localSettingsPath)) {
try {
const localSettings = JSON.parse(fs.readFileSync(localSettingsPath, 'utf8'));
localTheme = localSettings.ui?.theme || localTheme;
useAlternateBuffer = localSettings.ui?.useAlternateBuffer ?? useAlternateBuffer;
useBackgroundColor = localSettings.ui?.useBackgroundColor ?? useBackgroundColor;
} catch (e) {}
}
const remoteSettings: any = {
security: {
auth: {
selectedType: authStrategy
},
folderTrust: {
enabled: false
}
},
ui: {
theme: localTheme,
useAlternateBuffer,
useBackgroundColor,
},
general: {
enableAutoUpdate: false
}
};
if (authStrategy === 'gemini-api-key' && geminiApiKey) {
remoteSettings.security.auth.apiKey = geminiApiKey;
console.log(' ✅ Configuring remote for API Key authentication.');
}
const tmpSettingsPath = path.join(os.tmpdir(), `remote-settings-${Date.now()}.json`);
fs.writeFileSync(tmpSettingsPath, JSON.stringify(remoteSettings, null, 2));
// Ensure the remote config dir exists before syncing
await provider.exec(`sudo mkdir -p ${remoteConfigDir} && sudo chmod 777 ${remoteConfigDir}`);
await provider.sync(tmpSettingsPath, `${remoteConfigDir}/settings.json`, { sudo: true });
fs.unlinkSync(tmpSettingsPath);
// 3. Sync credentials for Google Accounts if needed
if (authStrategy === 'google_accounts' || authStrategy === 'oauth-personal') {
if (fs.existsSync(path.join(env.HOME || '', '.gemini/google_accounts.json'))) {
await provider.sync(path.join(env.HOME || '', '.gemini/google_accounts.json'), `${remoteConfigDir}/google_accounts.json`, { sudo: true });
console.log(' ✅ Synchronized Google Accounts credentials.');
}
}
if (githubToken) {
await provider.exec(`echo ${githubToken} | sudo tee ${workspaceRoot}/.gh_token > /dev/null && sudo chmod 600 ${workspaceRoot}/.gh_token`);
// Authenticate GH CLI on host
await provider.exec(`sudo -u $(whoami) gh auth login --with-token < ${workspaceRoot}/.gh_token`);
console.log(' ✅ Authenticated GitHub CLI on host.');
}
// Final Repo Sync
console.log(`🚀 Finalizing Remote Repository (${userFork})...`);
const repoUrl = `https://github.com/${userFork}.git`;
const cloneCmd = `sudo rm -rf ${workspaceRoot}/main && sudo git clone --quiet --filter=blob:none ${repoUrl} ${workspaceRoot}/main && sudo git -C ${workspaceRoot}/main remote add upstream https://github.com/${upstreamRepo}.git && sudo git -C ${workspaceRoot}/main fetch --quiet upstream && sudo chown -R 1000:1000 ${workspaceRoot}`;
await provider.exec(cloneCmd);
console.log('\n✨ ALL SYSTEMS GO! Your Gemini Workspace is ready.');
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
runSetup().catch(console.error);
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Workspace Status Inspector (Local)
*
* Orchestrates remote status retrieval via the WorkerProvider.
*/
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
async function runStatus(env: NodeJS.ProcessEnv = process.env) {
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Settings not found. Run "npm run workspace:setup" first.');
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const config = settings.workspace;
if (!config) {
console.error('❌ Deep Review configuration not found.');
return 1;
}
const { projectId, zone } = config;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
console.log(`\n🛰️ Workspace Mission Control: ${targetVM}`);
console.log(`--------------------------------------------------------------------------------`);
const status = await provider.getStatus();
console.log(` - VM State: ${status.status}`);
console.log(` - Internal IP: ${status.internalIp || 'N/A'}`);
if (status.status === 'RUNNING') {
console.log(`\n🧵 Active Sessions (tmux):`);
// We fetch the list of sessions from INSIDE the container
const tmuxRes = await provider.getExecOutput('tmux list-sessions -F "#S" 2>/dev/null', { wrapContainer: 'maintainer-worker' });
if (tmuxRes.status === 0 && tmuxRes.stdout.trim()) {
const sessions = tmuxRes.stdout.trim().split('\n');
sessions.forEach(s => {
if (s.startsWith('workspace-')) {
console.log(`${s}`);
} else {
console.log(` 🔹 ${s} (Non-workspace)`);
}
});
} else {
console.log(' - No active sessions');
}
}
console.log(`--------------------------------------------------------------------------------\n`);
return 0;
}
runStatus().catch(console.error);
+74
View File
@@ -0,0 +1,74 @@
/**
* Universal Workspace Worker (Remote)
*
* Stateful orchestrator for complex development loops.
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { runReviewPlaybook } from './playbooks/review.ts';
import { runFixPlaybook } from './playbooks/fix.ts';
import { runReadyPlaybook } from './playbooks/ready.ts';
export async function runWorker(args: string[]) {
const prNumberOrIssue = args[0];
const branchName = args[1];
const policyPath = args[2];
const action = args[3] || 'review';
if (!prNumberOrIssue || !policyPath) {
console.error('Usage: tsx worker.ts <ID> <BRANCH_NAME> <POLICY_PATH> [action]');
return 1;
}
const workDir = process.cwd();
// For 'implement', the ID is an issue number and we might not have a branch yet
const isImplement = action === 'implement';
const targetDir = isImplement ? workDir : path.join(workDir, branchName);
// 1. Provision Environment
if (!isImplement && !fs.existsSync(targetDir)) {
console.log(`🌿 Provisioning PR #${prNumberOrIssue} into ${branchName}...`);
const cloneCmd = `git clone --filter=blob:none https://github.com/google-gemini/gemini-cli.git ${targetDir}`;
spawnSync(cloneCmd, { stdio: 'inherit', shell: true });
process.chdir(targetDir);
spawnSync('gh', ['pr', 'checkout', prNumberOrIssue], { stdio: 'inherit' });
} else if (!isImplement) {
process.chdir(targetDir);
}
// Use global gemini command pre-installed in the maintainer image
const geminiBin = 'gemini';
// 2. Dispatch to Playbook
switch (action) {
case 'review':
return runReviewPlaybook(prNumberOrIssue, targetDir, policyPath, geminiBin);
case 'fix':
// The 'fix' playbook now handles its own internal loop
return runFixPlaybook(prNumberOrIssue, targetDir, policyPath, geminiBin);
case 'ready':
return runReadyPlaybook(prNumberOrIssue, targetDir, policyPath, geminiBin);
case 'implement':
// Lazy-load implement playbook (to be created)
const { runImplementPlaybook } = await import('./playbooks/implement.ts');
return runImplementPlaybook(prNumberOrIssue, workDir, policyPath, geminiBin);
case 'open':
console.log(`🚀 Dropping into manual session...`);
return 0;
default:
console.error(`❌ Unknown action: ${action}`);
return 1;
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
runWorker(process.argv.slice(2)).catch(console.error);
}
@@ -0,0 +1,58 @@
---
name: workspaces
description: Expertise in managing and utilizing Gemini Workspaces for high-performance remote development tasks.
---
# Gemini Workspaces Skill
This skill enables the agent to utilize **Gemini Workspaces**—a high-performance, persistent remote development platform. It allows the agent to move intensive tasks (PR reviews, complex repairs, full builds) from the local environment to a dedicated cloud worker.
## 🛠️ Key Capabilities
1. **Persistent Execution**: Jobs run in remote `tmux` sessions. Disconnecting or crashing the local terminal does not stop the remote work.
2. **Parallel Infrastructure**: The agent can launch a heavy task (like a full build or CI run) in a workspace while continuing to assist the user locally.
3. **Behavioral Fidelity**: Remote workers have full tool access (Git, Node, Docker, etc.) and high-performance compute, allowing the agent to provide behavioral proofs of its work.
## 📋 Instructions for the Agent
### When to use Workspaces
- **Intensive Tasks**: Full preflight runs, large-scale refactors, or deep PR reviews.
- **Persistent Logic**: When a task is expected to take longer than a few minutes and needs to survive local connection drops.
- **Environment Isolation**: When you need a clean, high-performance environment to verify a fix without polluting the user's local machine.
### How to use Workspaces
1. **Setup**: If the user hasn't initialized their environment, instruct them to run the setup script:
```bash
npx tsx ${extensionPath}/extensions/workspaces/scripts/setup.ts
```
2. **Launch**: Start a playbook for a specific PR/issue:
```bash
npx tsx ${extensionPath}/extensions/workspaces/scripts/orchestrator.ts <PR_NUMBER> [action]
```
- Actions: `review` (default), `fix`, `ready`.
3. **Check Status**: See global state and active sessions:
```bash
npx tsx ${extensionPath}/extensions/workspaces/scripts/status.ts
```
Or deep-dive into specific PR logs:
```bash
npx tsx ${extensionPath}/extensions/workspaces/scripts/check.ts <PR_NUMBER>
```
4. **Cleanup**:
- **Bulk**: Clear all sessions/worktrees:
```bash
npx tsx ${extensionPath}/extensions/workspaces/scripts/clean.ts --all
```
- **Surgical**: Kill a specific PR task:
```bash
npx tsx ${extensionPath}/extensions/workspaces/scripts/clean.ts <PR_NUMBER> <action>
```
5. **Fleet**: Manage VM lifecycle:
```bash
npx tsx ${extensionPath}/extensions/workspaces/scripts/fleet.ts [stop|provision|list]
```
## ⚠️ Important Constraints
- **Absolute Paths**: Always use absolute paths (e.g., `/mnt/disks/data/...`) when orchestrating remote commands.
- **npx tsx**: When running scripts manually from the skill directory, always prefix with `npx tsx` to ensure dependencies are available.
- **Be Behavioral**: Prioritize results from live execution (behavioral proofs) over static reading.
- **Multi-tasking**: Remind the user they can continue chatting in the main window while the heavy workspace task runs in the separate terminal window.
+11
View File
@@ -0,0 +1,11 @@
{
"name": "workspaces",
"version": "0.1.0",
"description": "High-performance remote development workspaces for Gemini CLI.",
"author": "Google Gemini Team",
"license": "Apache-2.0",
"skills": [
"extensions/workspaces/skills/workspaces/SKILL.md"
],
"contextFileName": "extensions/workspaces/docs/GEMINI.md"
}
@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"file_path":"test.txt","content":"hello"}}},{"text":"I've successfully written \"hello\" to test.txt. The file has been created with the specified content."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
+29
View File
@@ -203,4 +203,33 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
// Should successfully complete all operations
assertModelHasOutput(result);
});
it('should handle tool confirmation for write_file without crashing', async () => {
rig.setup('tool-confirmation', {
fakeResponsesPath: join(
__dirname,
'browser-agent.confirmation.responses',
),
settings: {
agents: {
browser_agent: {
headless: true,
sessionMode: 'isolated',
},
},
},
});
const run = await rig.runInteractive({ approvalMode: 'default' });
await run.type('Write hello to test.txt');
await run.type('\r');
await run.expectText('Allow', 15000);
await run.type('y');
await run.type('\r');
await run.expectText('successfully written', 15000);
});
});
+4 -5
View File
@@ -42,11 +42,10 @@ describe('extension install', () => {
const listResult = await rig.runCommand(['extensions', 'list']);
expect(listResult).toContain('test-extension-install');
writeFileSync(testServerPath, extensionUpdate);
const updateResult = await rig.runCommand([
'extensions',
'update',
`test-extension-install`,
]);
const updateResult = await rig.runCommand(
['extensions', 'update', `test-extension-install`],
{ stdin: 'y\n' },
);
expect(updateResult).toContain('0.0.2');
} finally {
await rig.runCommand([
+549 -22
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"workspaces": [
"packages/*"
],
@@ -3038,6 +3038,27 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@puppeteer/browsers": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz",
"integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.3",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.4",
"tar-fs": "^3.1.1",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/cjs/main-cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
@@ -3762,6 +3783,12 @@
"node": ">= 10"
}
},
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
"node_modules/@ts-morph/common": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz",
@@ -3949,6 +3976,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-stable-stringify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz",
"integrity": "sha512-ESTsHWB72QQq+pjUFIbEz9uSCZppD31YrVkbt2rnUciTYEvcwN6uZIhX5JZeBHqRlFJ41x/7MewCs7E2Qux6Cg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -5584,6 +5618,18 @@
"node": ">=12"
}
},
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/ast-v8-to-istanbul": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz",
@@ -5676,6 +5722,20 @@
"typed-rest-client": "^1.8.4"
}
},
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -5685,6 +5745,93 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
"integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz",
"integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz",
"integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==",
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.21.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -5705,6 +5852,15 @@
],
"license": "MIT"
},
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
@@ -5895,7 +6051,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
@@ -6103,6 +6258,32 @@
"node": ">=18"
}
},
"node_modules/chrome-devtools-mcp": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-0.19.0.tgz",
"integrity": "sha512-LfqjOxdUjWvCQrfeI5V3ZBJCUIDKGNmexSbSAgsrjVggN4X1OSObLxleSlX2zwcXRZYxqy209cww0MXcXuN1zw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"chrome-devtools-mcp": "build/src/index.js"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/chromium-bidi": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz",
"integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cjs-module-lexer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
@@ -6901,7 +7082,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@@ -6945,6 +7125,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -7204,6 +7398,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1581282",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@@ -7759,6 +7959,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/eslint": {
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
@@ -8118,7 +8339,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -8137,7 +8357,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@@ -8189,6 +8408,15 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@@ -8395,6 +8623,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -9037,6 +9271,29 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/get-uri/node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/glob": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz",
@@ -9461,7 +9718,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@@ -9663,7 +9919,6 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
@@ -10577,7 +10832,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
@@ -10801,6 +11055,25 @@
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
"license": "BSD-2-Clause"
},
"node_modules/json-stable-stringify": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -10849,6 +11122,15 @@
"node": ">= 10.0.0"
}
},
"node_modules/jsonify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
"license": "Public Domain",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -11759,6 +12041,12 @@
"node": ">= 18"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -11959,6 +12247,15 @@
"node": ">= 0.6"
}
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
@@ -12401,7 +12698,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -12662,6 +12958,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
"license": "MIT",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.6",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"license": "MIT",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/package-json": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz",
@@ -13132,6 +13460,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -13237,6 +13574,40 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.6",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.1.0",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-agent/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -13290,6 +13661,45 @@
"node": ">=6"
}
},
"node_modules/puppeteer-core": {
"version": "24.39.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.39.0.tgz",
"integrity": "sha512-SzIxz76Kgu17HUIi57HOejPiN0JKa9VCd2GcPY1sAh6RA4BzGZarFQdOYIYrBdUVbtyH7CrDb9uhGEwVXK/YNA==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.13.0",
"chromium-bidi": "14.0.0",
"debug": "^4.4.3",
"devtools-protocol": "0.0.1581282",
"typed-query-selector": "^2.12.1",
"webdriver-bidi-protocol": "0.4.1",
"ws": "^8.19.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/puppeteer-core/node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
@@ -14250,9 +14660,9 @@
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -14317,7 +14727,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@@ -14583,6 +14992,54 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -14711,6 +15168,17 @@
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
"license": "MIT"
},
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/strict-event-emitter": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
@@ -15308,6 +15776,32 @@
"node": ">=8"
}
},
"node_modules/tar-fs": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
"integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/teeny-request": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
@@ -15363,6 +15857,15 @@
"node": ">= 6"
}
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/terminal-link": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz",
@@ -15395,6 +15898,15 @@
"node": ">=18"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -15719,7 +16231,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/tsx": {
@@ -15869,6 +16380,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typed-query-selector": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz",
"integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==",
"license": "MIT"
},
"node_modules/typed-rest-client": {
"version": "1.8.11",
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz",
@@ -16336,6 +16853,12 @@
}
}
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz",
"integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==",
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -16890,7 +17413,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "^7.16.0",
@@ -17005,7 +17528,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -17177,7 +17700,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -17226,12 +17749,14 @@
"ignore": "^7.0.0",
"ipaddr.js": "^1.9.1",
"js-yaml": "^4.1.1",
"json-stable-stringify": "^1.3.0",
"marked": "^15.0.12",
"mime": "4.0.7",
"mnemonist": "^0.40.3",
"open": "^10.1.2",
"picomatch": "^4.0.1",
"proper-lockfile": "^4.1.2",
"puppeteer-core": "^24.0.0",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.3",
"simple-git": "^3.28.0",
@@ -17249,7 +17774,9 @@
"@google/gemini-cli-test-utils": "file:../test-utils",
"@types/fast-levenshtein": "^0.0.4",
"@types/js-yaml": "^4.0.9",
"@types/json-stable-stringify": "^1.1.0",
"@types/picomatch": "^4.0.1",
"chrome-devtools-mcp": "^0.19.0",
"msw": "^2.3.4",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
@@ -17439,7 +17966,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
@@ -17454,7 +17981,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17471,7 +17998,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -17488,7 +18015,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+13 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260311.657f19c1f"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -43,6 +43,7 @@
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts && npm run test:sea-launch",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
"test:sea-launch": "vitest run sea/sea-launch.test.js",
"posttest": "npm run build",
"test:always_passing_evals": "vitest run --config evals/vitest.config.ts",
"test:all_evals": "cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts",
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
@@ -63,6 +64,16 @@
"telemetry": "node scripts/telemetry.js",
"check:lockfile": "node scripts/check-lockfile.js",
"clean": "node scripts/clean.js",
"workspace": "tsx ./scripts/workspaces.ts",
"workspace:setup": "tsx ./scripts/workspaces.ts setup",
"workspace:shell": "tsx ./scripts/workspaces.ts shell",
"workspace:check": "tsx ./scripts/workspaces.ts check",
"workspace:clean-all": "tsx ./scripts/workspaces.ts clean-all",
"workspace:kill": "tsx ./scripts/workspaces.ts kill",
"workspace:fleet": "tsx ./scripts/workspaces.ts fleet",
"workspace:status": "tsx ./scripts/workspaces.ts status",
"workspace:attach": "tsx ./scripts/workspaces.ts attach",
"workspace:logs": "tsx ./scripts/workspaces.ts logs",
"pre-commit": "node scripts/pre-commit.js"
},
"overrides": {
+22
View File
@@ -0,0 +1,22 @@
# Gemini CLI A2A Server (`@google/gemini-cli-a2a-server`)
Experimental Agent-to-Agent (A2A) server that exposes Gemini CLI capabilities
over HTTP for inter-agent communication.
## Architecture
- `src/agent/`: Agent session management for A2A interactions.
- `src/commands/`: CLI command definitions for the A2A server binary.
- `src/config/`: Server configuration.
- `src/http/`: HTTP server and route handlers.
- `src/persistence/`: Session and state persistence.
- `src/utils/`: Shared utility functions.
- `src/types.ts`: Shared type definitions.
## Running
- Binary entry point: `gemini-cli-a2a-server`
## Testing
- Run tests: `npm test -w @google/gemini-cli-a2a-server`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -177,10 +177,13 @@ describe('a2a-server memory commands', () => {
expect.any(AbortSignal),
undefined,
{
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
shellExecutionConfig: {
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
sandboxManager: undefined,
},
},
);
+4 -1
View File
@@ -103,7 +103,10 @@ export class AddMemoryCommand implements Command {
const abortController = new AbortController();
const signal = abortController.signal;
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: loopContext.sandboxManager,
},
});
await refreshMemory(context.config);
return {
@@ -19,6 +19,8 @@ import {
AuthType,
isHeadlessMode,
FatalAuthenticationError,
PolicyDecision,
PRIORITY_YOLO_ALLOW_ALL,
} from '@google/gemini-cli-core';
// Mock dependencies
@@ -325,6 +327,29 @@ describe('loadConfig', () => {
);
});
it('should pass enableAgents to Config constructor', async () => {
const settings: Settings = {
experimental: {
enableAgents: false,
},
};
await loadConfig(settings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
enableAgents: false,
}),
);
});
it('should default enableAgents to true when not provided', async () => {
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
enableAgents: true,
}),
);
});
describe('interactivity', () => {
it('should set interactive true when not headless', async () => {
vi.mocked(isHeadlessMode).mockReturnValue(false);
@@ -349,6 +374,41 @@ describe('loadConfig', () => {
});
});
describe('YOLO mode', () => {
it('should enable YOLO mode and add policy rule when GEMINI_YOLO_MODE is true', async () => {
vi.stubEnv('GEMINI_YOLO_MODE', 'true');
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
approvalMode: 'yolo',
policyEngineConfig: expect.objectContaining({
rules: expect.arrayContaining([
expect.objectContaining({
decision: PolicyDecision.ALLOW,
priority: PRIORITY_YOLO_ALLOW_ALL,
modes: ['yolo'],
allowRedirection: true,
}),
]),
}),
}),
);
});
it('should use default approval mode and empty rules when GEMINI_YOLO_MODE is not true', async () => {
vi.stubEnv('GEMINI_YOLO_MODE', 'false');
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
approvalMode: 'default',
policyEngineConfig: expect.objectContaining({
rules: [],
}),
}),
);
});
});
describe('authentication fallback', () => {
beforeEach(() => {
vi.stubEnv('USE_CCPA', 'true');
+22 -4
View File
@@ -26,6 +26,8 @@ import {
isHeadlessMode,
FatalAuthenticationError,
isCloudShell,
PolicyDecision,
PRIORITY_YOLO_ALLOW_ALL,
type TelemetryTarget,
type ConfigParameters,
type ExtensionLoader,
@@ -60,6 +62,11 @@ export async function loadConfig(
}
}
const approvalMode =
process.env['GEMINI_YOLO_MODE'] === 'true'
? ApprovalMode.YOLO
: ApprovalMode.DEFAULT;
const configParams: ConfigParameters = {
sessionId: taskId,
clientName: 'a2a-server',
@@ -74,10 +81,20 @@ export async function loadConfig(
excludeTools: settings.excludeTools || settings.tools?.exclude || undefined,
allowedTools: settings.allowedTools || settings.tools?.allowed || undefined,
showMemoryUsage: settings.showMemoryUsage || false,
approvalMode:
process.env['GEMINI_YOLO_MODE'] === 'true'
? ApprovalMode.YOLO
: ApprovalMode.DEFAULT,
approvalMode,
policyEngineConfig: {
rules:
approvalMode === ApprovalMode.YOLO
? [
{
decision: PolicyDecision.ALLOW,
priority: PRIORITY_YOLO_ALLOW_ALL,
modes: [ApprovalMode.YOLO],
allowRedirection: true,
},
]
: [],
},
mcpServers: settings.mcpServers,
cwd: workspaceDir,
telemetry: {
@@ -110,6 +127,7 @@ export async function loadConfig(
interactive: !isHeadlessMode(),
enableInteractiveShell: !isHeadlessMode(),
ptyInfo: 'auto',
enableAgents: settings.experimental?.enableAgents ?? true,
};
const fileService = new FileDiscoveryService(workspaceDir, {
@@ -112,6 +112,18 @@ describe('loadSettings', () => {
expect(result.fileFiltering?.respectGitIgnore).toBe(true);
});
it('should load experimental settings correctly', () => {
const settings = {
experimental: {
enableAgents: true,
},
};
fs.writeFileSync(USER_SETTINGS_PATH, JSON.stringify(settings));
const result = loadSettings(mockWorkspaceDir);
expect(result.experimental?.enableAgents).toBe(true);
});
it('should overwrite top-level settings from workspace (shallow merge)', () => {
const userSettings = {
showMemoryUsage: false,
@@ -48,6 +48,9 @@ export interface Settings {
enableRecursiveFileSearch?: boolean;
customIgnoreFilePaths?: string[];
};
experimental?: {
enableAgents?: boolean;
};
}
export interface SettingsError {
@@ -21,7 +21,9 @@ import {
tmpdir,
type Config,
type Storage,
NoopSandboxManager,
type ToolRegistry,
type SandboxManager,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
import { expect, vi } from 'vitest';
@@ -97,6 +99,15 @@ export function createMockConfig(
}),
getGitService: vi.fn(),
validatePathAccess: vi.fn().mockReturnValue(undefined),
getShellExecutionConfig: vi.fn().mockReturnValue({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
sandboxManager: new NoopSandboxManager() as unknown as SandboxManager,
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
enableEnvironmentVariableRedaction: false,
},
}),
...overrides,
} as unknown as Config;
+1 -1
View File
@@ -5,7 +5,7 @@
- Always fix react-hooks/exhaustive-deps lint errors by adding the missing
dependencies.
- **Shortcuts**: only define keyboard shortcuts in
`packages/cli/src/config/keyBindings.ts`
`packages/cli/src/ui/key/keyBindings.ts`
- Do not implement any logic performing custom string measurement or string
truncation. Use Ink layout instead leveraging ResizeObserver as needed.
- Avoid prop drilling when at all possible.
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.35.0-nightly.20260311.657f19c1f",
"version": "0.36.0-nightly.20260317.2f90b4653",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -20,13 +20,14 @@
"format": "prettier --write .",
"test": "vitest run",
"test:ci": "vitest run",
"posttest": "npm run build",
"typecheck": "tsc --noEmit"
},
"files": [
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.35.0-nightly.20260311.657f19c1f"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
+57
View File
@@ -176,6 +176,7 @@ describe('GeminiAgent', () => {
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
mockSettings = {
merged: {
@@ -654,6 +655,7 @@ describe('Session', () => {
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getGitService: vi.fn().mockResolvedValue({} as GitService),
waitForMcpInit: vi.fn(),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
} as unknown as Mocked<Config>;
mockConnection = {
sessionUpdate: vi.fn(),
@@ -947,6 +949,61 @@ describe('Session', () => {
);
});
it('should exclude always allow options when disableAlwaysAllow is true', async () => {
mockConfig.getDisableAlwaysAllow = vi.fn().mockReturnValue(true);
const confirmationDetails = {
type: 'info',
onConfirm: vi.fn(),
};
mockTool.build.mockReturnValue({
getDescription: () => 'Test Tool',
toolLocations: () => [],
shouldConfirmExecute: vi.fn().mockResolvedValue(confirmationDetails),
execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }),
});
mockConnection.requestPermission.mockResolvedValue({
outcome: {
outcome: 'selected',
optionId: ToolConfirmationOutcome.ProceedOnce,
},
});
const stream1 = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
functionCalls: [{ name: 'test_tool', args: {} }],
},
},
]);
const stream2 = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [] },
},
]);
mockChat.sendMessageStream
.mockResolvedValueOnce(stream1)
.mockResolvedValueOnce(stream2);
await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Call tool' }],
});
expect(mockConnection.requestPermission).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.not.arrayContaining([
expect.objectContaining({
optionId: ToolConfirmationOutcome.ProceedAlways,
}),
]),
}),
);
});
it('should use filePath for ACP diff content in permission request', async () => {
const confirmationDetails = {
type: 'edit',
+56 -38
View File
@@ -908,7 +908,7 @@ export class Session {
const params: acp.RequestPermissionRequest = {
sessionId: this.id,
options: toPermissionOptions(confirmationDetails),
options: toPermissionOptions(confirmationDetails, this.config),
toolCall: {
toolCallId: callId,
status: 'pending',
@@ -1004,6 +1004,7 @@ export class Session {
callId,
toolResult.llmContent,
this.config.getActiveModel(),
this.config,
),
resultDisplay: toolResult.returnDisplay,
error: undefined,
@@ -1017,6 +1018,7 @@ export class Session {
callId,
toolResult.llmContent,
this.config.getActiveModel(),
this.config,
);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
@@ -1457,60 +1459,76 @@ const basicPermissionOptions = [
function toPermissionOptions(
confirmation: ToolCallConfirmationDetails,
config: Config,
): acp.PermissionOption[] {
switch (confirmation.type) {
case 'edit':
return [
{
const disableAlwaysAllow = config.getDisableAlwaysAllow();
const options: acp.PermissionOption[] = [];
if (!disableAlwaysAllow) {
switch (confirmation.type) {
case 'edit':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: 'Allow All Edits',
kind: 'allow_always',
},
...basicPermissionOptions,
];
case 'exec':
return [
{
});
break;
case 'exec':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: `Always Allow ${confirmation.rootCommand}`,
kind: 'allow_always',
},
...basicPermissionOptions,
];
case 'mcp':
return [
{
optionId: ToolConfirmationOutcome.ProceedAlwaysServer,
name: `Always Allow ${confirmation.serverName}`,
kind: 'allow_always',
},
{
optionId: ToolConfirmationOutcome.ProceedAlwaysTool,
name: `Always Allow ${confirmation.toolName}`,
kind: 'allow_always',
},
...basicPermissionOptions,
];
case 'info':
return [
{
});
break;
case 'mcp':
options.push(
{
optionId: ToolConfirmationOutcome.ProceedAlwaysServer,
name: `Always Allow ${confirmation.serverName}`,
kind: 'allow_always',
},
{
optionId: ToolConfirmationOutcome.ProceedAlwaysTool,
name: `Always Allow ${confirmation.toolName}`,
kind: 'allow_always',
},
);
break;
case 'info':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: `Always Allow`,
kind: 'allow_always',
},
...basicPermissionOptions,
];
});
break;
case 'ask_user':
case 'exit_plan_mode':
// askuser and exit_plan_mode don't need "always allow" options
break;
default:
// No "always allow" options for other types
break;
}
}
options.push(...basicPermissionOptions);
// Exhaustive check
switch (confirmation.type) {
case 'edit':
case 'exec':
case 'mcp':
case 'info':
case 'ask_user':
// askuser doesn't need "always allow" options since it's asking questions
return [...basicPermissionOptions];
case 'exit_plan_mode':
// exit_plan_mode doesn't need "always allow" options since it's a plan approval flow
return [...basicPermissionOptions];
break;
default: {
const unreachable: never = confirmation;
throw new Error(`Unexpected: ${unreachable}`);
}
}
return options;
}
/**
+5 -2
View File
@@ -4,13 +4,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { listExtensions, type Config } from '@google/gemini-cli-core';
import {
listExtensions,
type Config,
getErrorMessage,
} from '@google/gemini-cli-core';
import { SettingScope } from '../../config/settings.js';
import {
ExtensionManager,
inferInstallMetadata,
} from '../../config/extension-manager.js';
import { getErrorMessage } from '../../utils/errors.js';
import { McpServerEnablementManager } from '../../config/mcp/mcpServerEnablement.js';
import { stat } from 'node:fs/promises';
import type {
+4 -1
View File
@@ -104,7 +104,10 @@ export class AddMemoryCommand implements Command {
await context.sendMessage(`Saving memory via ${result.toolName}...`);
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: context.config.sandboxManager,
},
});
await refreshMemory(context.config);
return {
@@ -22,7 +22,7 @@ import {
SettingScope,
type LoadedSettings,
} from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
import { getErrorMessage } from '@google/gemini-cli-core';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
@@ -44,12 +44,12 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
emitConsoleLog,
},
debugLogger,
getErrorMessage: vi.fn(),
};
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
@@ -6,8 +6,7 @@
import { type CommandModule } from 'yargs';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
import { debugLogger } from '@google/gemini-cli-core';
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import { ExtensionManager } from '../../config/extension-manager.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
@@ -137,6 +137,7 @@ describe('handleInstall', () => {
mcps: [],
hooks: [],
skills: [],
agents: [],
settings: [],
securityWarnings: [],
discoveryErrors: [],
@@ -379,6 +380,7 @@ describe('handleInstall', () => {
mcps: [],
hooks: [],
skills: ['cool-skill'],
agents: ['cool-agent'],
settings: [],
securityWarnings: ['Security risk!'],
discoveryErrors: ['Read error'],
@@ -408,6 +410,10 @@ describe('handleInstall', () => {
expect.stringContaining('cool-skill'),
false,
);
expect(mockPromptForConsentNonInteractive).toHaveBeenCalledWith(
expect.stringContaining('cool-agent'),
false,
);
expect(mockPromptForConsentNonInteractive).toHaveBeenCalledWith(
expect.stringContaining('Security Warnings:'),
false,
@@ -11,8 +11,8 @@ import {
debugLogger,
FolderTrustDiscoveryService,
getRealPath,
getErrorMessage,
} from '@google/gemini-cli-core';
import { getErrorMessage } from '../../utils/errors.js';
import {
INSTALL_WARNING_MESSAGE,
promptForConsentNonInteractive,
@@ -99,11 +99,15 @@ export async function handleInstall(args: InstallArgs) {
if (hasDiscovery) {
promptLines.push(chalk.bold('This folder contains:'));
const groups = [
{ label: 'Commands', items: discoveryResults.commands },
{ label: 'MCP Servers', items: discoveryResults.mcps },
{ label: 'Hooks', items: discoveryResults.hooks },
{ label: 'Skills', items: discoveryResults.skills },
{ label: 'Setting overrides', items: discoveryResults.settings },
{ label: 'Commands', items: discoveryResults.commands ?? [] },
{ label: 'MCP Servers', items: discoveryResults.mcps ?? [] },
{ label: 'Hooks', items: discoveryResults.hooks ?? [] },
{ label: 'Skills', items: discoveryResults.skills ?? [] },
{ label: 'Agents', items: discoveryResults.agents ?? [] },
{
label: 'Setting overrides',
items: discoveryResults.settings ?? [],
},
].filter((g) => g.items.length > 0);
for (const group of groups) {
@@ -13,26 +13,24 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { coreEvents } from '@google/gemini-cli-core';
import { coreEvents, getErrorMessage } from '@google/gemini-cli-core';
import { type Argv } from 'yargs';
import { handleLink, linkCommand } from './link.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const { mockCoreDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return mockCoreDebugLogger(
await importOriginal<typeof import('@google/gemini-cli-core')>(),
{ stripAnsi: true },
);
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
const mocked = mockCoreDebugLogger(actual, { stripAnsi: true });
return { ...mocked, getErrorMessage: vi.fn() };
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
+1 -1
View File
@@ -8,10 +8,10 @@ import type { CommandModule } from 'yargs';
import chalk from 'chalk';
import {
debugLogger,
getErrorMessage,
type ExtensionInstallMetadata,
} from '@google/gemini-cli-core';
import { getErrorMessage } from '../../utils/errors.js';
import {
INSTALL_WARNING_MESSAGE,
requestConsentNonInteractive,
@@ -5,27 +5,23 @@
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { coreEvents } from '@google/gemini-cli-core';
import { coreEvents, getErrorMessage } from '@google/gemini-cli-core';
import { handleList, listCommand } from './list.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const { mockCoreDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return mockCoreDebugLogger(
await importOriginal<typeof import('@google/gemini-cli-core')>(),
{
stripAnsi: false,
},
);
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
const mocked = mockCoreDebugLogger(actual, { stripAnsi: false });
return { ...mocked, getErrorMessage: vi.fn() };
});
vi.mock('../../config/extension-manager.js');
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
+1 -2
View File
@@ -5,8 +5,7 @@
*/
import type { CommandModule } from 'yargs';
import { getErrorMessage } from '../../utils/errors.js';
import { debugLogger } from '@google/gemini-cli-core';
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import { ExtensionManager } from '../../config/extension-manager.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { loadSettings } from '../../config/settings.js';
@@ -18,7 +18,7 @@ import { type Argv } from 'yargs';
import { handleUninstall, uninstallCommand } from './uninstall.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
import { getErrorMessage } from '@google/gemini-cli-core';
// NOTE: This file uses vi.hoisted() mocks to enable testing of sequential
// mock behaviors (mockResolvedValueOnce/mockRejectedValueOnce chaining).
@@ -66,11 +66,11 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
emitConsoleLog,
},
debugLogger,
getErrorMessage: vi.fn(),
};
});
vi.mock('../../config/settings.js');
vi.mock('../../utils/errors.js');
vi.mock('../../config/extensions/consent.js', () => ({
requestConsentNonInteractive: vi.fn(),
}));
@@ -5,8 +5,7 @@
*/
import type { CommandModule } from 'yargs';
import { getErrorMessage } from '../../utils/errors.js';
import { debugLogger } from '@google/gemini-cli-core';
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings } from '../../config/settings.js';
@@ -12,9 +12,12 @@ import {
updateExtension,
} from '../../config/extensions/update.js';
import { checkForExtensionUpdate } from '../../config/extensions/github.js';
import { getErrorMessage } from '../../utils/errors.js';
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
import {
coreEvents,
debugLogger,
getErrorMessage,
} from '@google/gemini-cli-core';
import { ExtensionManager } from '../../config/extension-manager.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { loadSettings } from '../../config/settings.js';
@@ -5,11 +5,10 @@
*/
import type { CommandModule } from 'yargs';
import { debugLogger } from '@google/gemini-cli-core';
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import semver from 'semver';
import { getErrorMessage } from '../../utils/errors.js';
import type { ExtensionConfig } from '../../config/extension.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
@@ -28,6 +28,9 @@ const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => {
vi.mock('@google/gemini-cli-core', () => ({
debugLogger,
getErrorMessage: vi.fn((e: unknown) =>
e instanceof Error ? e.message : String(e),
),
}));
import { handleInstall, installCommand } from './install.js';
+5 -2
View File
@@ -5,8 +5,11 @@
*/
import type { CommandModule } from 'yargs';
import { debugLogger, type SkillDefinition } from '@google/gemini-cli-core';
import { getErrorMessage } from '../../utils/errors.js';
import {
debugLogger,
type SkillDefinition,
getErrorMessage,
} from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
import { installSkill } from '../../utils/skillUtils.js';
import chalk from 'chalk';
@@ -24,6 +24,9 @@ const { debugLogger } = await vi.hoisted(async () => {
vi.mock('@google/gemini-cli-core', () => ({
debugLogger,
getErrorMessage: vi.fn((e: unknown) =>
e instanceof Error ? e.message : String(e),
),
}));
vi.mock('../../config/extensions/consent.js', () => ({
+1 -2
View File
@@ -5,10 +5,9 @@
*/
import type { CommandModule } from 'yargs';
import { debugLogger } from '@google/gemini-cli-core';
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import chalk from 'chalk';
import { getErrorMessage } from '../../utils/errors.js';
import { exitCli } from '../utils.js';
import {
requestConsentNonInteractive,
@@ -21,6 +21,9 @@ const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => {
vi.mock('@google/gemini-cli-core', () => ({
debugLogger,
getErrorMessage: vi.fn((e: unknown) =>
e instanceof Error ? e.message : String(e),
),
}));
import { handleUninstall, uninstallCommand } from './uninstall.js';
@@ -5,8 +5,7 @@
*/
import type { CommandModule } from 'yargs';
import { debugLogger } from '@google/gemini-cli-core';
import { getErrorMessage } from '../../utils/errors.js';
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js';
import { uninstallSkill } from '../../utils/skillUtils.js';
import chalk from 'chalk';
+57 -6
View File
@@ -763,6 +763,48 @@ describe('loadCliConfig', () => {
});
});
it('should add IDE workspace folders from GEMINI_CLI_IDE_WORKSPACE_PATH to include directories', async () => {
vi.stubEnv(
'GEMINI_CLI_IDE_WORKSPACE_PATH',
['/project/folderA', '/project/folderB'].join(path.delimiter),
);
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings();
const config = await loadCliConfig(settings, 'test-session', argv);
const dirs = config.getPendingIncludeDirectories();
expect(dirs).toContain('/project/folderA');
expect(dirs).toContain('/project/folderB');
});
it('should skip inaccessible workspace folders from GEMINI_CLI_IDE_WORKSPACE_PATH', async () => {
const resolveToRealPathSpy = vi
.spyOn(ServerConfig, 'resolveToRealPath')
.mockImplementation((p) => {
if (p.toString().includes('restricted')) {
const err = new Error('EACCES: permission denied');
(err as NodeJS.ErrnoException).code = 'EACCES';
throw err;
}
return p.toString();
});
vi.stubEnv(
'GEMINI_CLI_IDE_WORKSPACE_PATH',
['/project/folderA', '/nonexistent/restricted/folder'].join(
path.delimiter,
),
);
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings();
const config = await loadCliConfig(settings, 'test-session', argv);
const dirs = config.getPendingIncludeDirectories();
expect(dirs).toContain('/project/folderA');
expect(dirs).not.toContain('/nonexistent/restricted/folder');
resolveToRealPathSpy.mockRestore();
});
it('should use default fileFilter options when unconfigured', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
@@ -798,6 +840,7 @@ describe('loadCliConfig', () => {
describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '');
// Restore ExtensionManager mocks that were reset
ExtensionManager.prototype.getExtensions = vi.fn().mockReturnValue([]);
ExtensionManager.prototype.loadExtensions = vi
@@ -809,12 +852,15 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings();
const settings = createTestMergedSettings({
experimental: { jitContext: false },
});
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
{
path: '/path/to/ext1',
@@ -865,6 +911,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
process.argv = ['node', 'script.js'];
const includeDir = path.resolve(path.sep, 'path', 'to', 'include');
const settings = createTestMergedSettings({
experimental: { jitContext: false },
context: {
includeDirectories: [includeDir],
loadMemoryFromIncludeDirectories: true,
@@ -892,6 +939,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
it('should NOT pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is false', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
experimental: { jitContext: false },
context: {
includeDirectories: ['/path/to/include'],
loadMemoryFromIncludeDirectories: false,
@@ -1773,7 +1821,7 @@ describe('loadCliConfig model selection', () => {
});
it('always prefers model from argv', async () => {
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash'];
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash-preview'];
const argv = await parseArguments(createTestMergedSettings());
const config = await loadCliConfig(
createTestMergedSettings({
@@ -1785,11 +1833,11 @@ describe('loadCliConfig model selection', () => {
argv,
);
expect(config.getModel()).toBe('gemini-2.5-flash');
expect(config.getModel()).toBe('gemini-2.5-flash-preview');
});
it('selects the model from argv if provided', async () => {
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash'];
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-flash-preview'];
const argv = await parseArguments(createTestMergedSettings());
const config = await loadCliConfig(
createTestMergedSettings({
@@ -1799,7 +1847,7 @@ describe('loadCliConfig model selection', () => {
argv,
);
expect(config.getModel()).toBe('gemini-2.5-flash');
expect(config.getModel()).toBe('gemini-2.5-flash-preview');
});
it('selects the default auto model if provided via auto alias', async () => {
@@ -3343,7 +3391,10 @@ describe('Policy Engine Integration in loadCliConfig', () => {
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
policyPaths: ['/path/to/policy1.toml', '/path/to/policy2.toml'],
policyPaths: [
path.normalize('/path/to/policy1.toml'),
path.normalize('/path/to/policy2.toml'),
],
}),
expect.anything(),
);
+57 -81
View File
@@ -31,8 +31,6 @@ import {
type HierarchicalMemory,
coreEvents,
GEMINI_MODEL_ALIAS_AUTO,
isValidModelOrAlias,
getValidModelsAndAliases,
getAdminErrorMessage,
isHeadlessMode,
Config,
@@ -127,12 +125,19 @@ export async function parseArguments(
.usage(
'Usage: gemini [options] [command]\n\nGemini CLI - Defaults to interactive mode. Use -p/--prompt for non-interactive (headless) mode.',
)
.option('debug', {
alias: 'd',
type: 'boolean',
description: 'Run in debug mode (open debug console with F12)',
default: false,
})
if (settings.experimental?.extensionManagement) {
yargsInstance.command(extensionsCommand);
}
if (settings.skills?.enabled ?? true) {
yargsInstance.command(skillsCommand);
}
// Register hooks command if hooks are enabled
if (settings.hooksConfig.enabled) {
yargsInstance.command(hooksCommand);
}
yargsInstance
.command('$0 [query..]', 'Launch Gemini CLI', (yargsInstance) =>
yargsInstance
.positional('query', {
@@ -246,10 +251,11 @@ export async function parseArguments(
// When --resume passed without a value (`gemini --resume`): value = "" (string)
// When --resume not passed at all: this `coerce` function is not called at all, and
// `yargsInstance.argv.resume` is undefined.
if (value === '') {
const trimmed = value.trim();
if (trimmed === '') {
return RESUME_LATEST;
}
return value;
return trimmed;
},
})
.option('list-sessions', {
@@ -301,56 +307,6 @@ export async function parseArguments(
description: 'Suppress the security warning when using --raw-output.',
}),
)
// Register MCP subcommands
.command(mcpCommand)
// Ensure validation flows through .fail() for clean UX
.fail((msg, err) => {
if (err) throw err;
throw new Error(msg);
})
.check((argv) => {
// The 'query' positional can be a string (for one arg) or string[] (for multiple).
// This guard safely checks if any positional argument was provided.
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const query = argv['query'] as string | string[] | undefined;
const hasPositionalQuery = Array.isArray(query)
? query.length > 0
: !!query;
if (argv['prompt'] && hasPositionalQuery) {
return 'Cannot use both a positional prompt and the --prompt (-p) flag together';
}
if (argv['prompt'] && argv['promptInteractive']) {
return 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together';
}
if (argv['yolo'] && argv['approvalMode']) {
return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.';
}
if (
argv['outputFormat'] &&
!['text', 'json', 'stream-json'].includes(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
argv['outputFormat'] as string,
)
) {
return `Invalid values:\n Argument: output-format, Given: "${argv['outputFormat']}", Choices: "text", "json", "stream-json"`;
}
return true;
});
if (settings.experimental?.extensionManagement) {
yargsInstance.command(extensionsCommand);
}
if (settings.skills?.enabled ?? true) {
yargsInstance.command(skillsCommand);
}
// Register hooks command if hooks are enabled
if (settings.hooksConfig.enabled) {
yargsInstance.command(hooksCommand);
}
yargsInstance
.version(await getVersion()) // This will enable the --version flag based on package.json
.alias('v', 'version')
.help()
@@ -431,8 +387,6 @@ export async function loadCliConfig(
const { cwd = process.cwd(), projectHooks } = options;
const debugMode = isDebugMode(argv);
const loadedSettings = loadSettings(cwd);
if (argv.sandbox) {
process.env['GEMINI_SANDBOX'] = 'true';
}
@@ -476,10 +430,32 @@ export async function loadCliConfig(
...settings.context?.fileFiltering,
};
//changes the includeDirectories to be absolute paths based on the cwd, and also include any additional directories specified via CLI args
const includeDirectories = (settings.context?.includeDirectories || [])
.map(resolvePath)
.concat((argv.includeDirectories || []).map(resolvePath));
// When running inside VSCode with multiple workspace folders,
// automatically add the other folders as include directories
// so Gemini has context of all open folders, not just the cwd.
const ideWorkspacePath = process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'];
if (ideWorkspacePath) {
const realCwd = resolveToRealPath(cwd);
const ideFolders = ideWorkspacePath.split(path.delimiter).filter((p) => {
const trimmedPath = p.trim();
if (!trimmedPath) return false;
try {
return resolveToRealPath(trimmedPath) !== realCwd;
} catch (e) {
debugLogger.debug(
`[IDE] Skipping inaccessible workspace folder: ${trimmedPath} (${e instanceof Error ? e.message : String(e)})`,
);
return false;
}
});
includeDirectories.push(...ideFolders);
}
const extensionManager = new ExtensionManager({
settings,
requestConsent: requestConsentNonInteractive,
@@ -496,11 +472,12 @@ export async function loadCliConfig(
.getExtensions()
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
const experimentalJitContext = settings.experimental?.jitContext ?? false;
const experimentalJitContext = settings.experimental.jitContext;
let extensionRegistryURI =
process.env['GEMINI_CLI_EXTENSION_REGISTRY_URI'] ??
(trustedFolder ? settings.experimental?.extensionRegistryURI : undefined);
let extensionRegistryURI: string | undefined = trustedFolder
? settings.experimental?.extensionRegistryURI
: undefined;
if (extensionRegistryURI && !extensionRegistryURI.startsWith('http')) {
extensionRegistryURI = resolveToRealPath(
path.resolve(cwd, resolvePath(extensionRegistryURI)),
@@ -651,8 +628,12 @@ export async function loadCliConfig(
...settings.mcp,
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
},
policyPaths: argv.policy ?? settings.policyPaths,
adminPolicyPaths: argv.adminPolicy ?? settings.adminPolicyPaths,
policyPaths: (argv.policy ?? settings.policyPaths)?.map((p) =>
resolvePath(p),
),
adminPolicyPaths: (argv.adminPolicy ?? settings.adminPolicyPaths)?.map(
(p) => resolvePath(p),
),
};
const { workspacePoliciesDir, policyUpdateConfirmationRequest } =
@@ -673,18 +654,6 @@ export async function loadCliConfig(
const specifiedModel =
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
// Validate the model if one was explicitly specified
if (specifiedModel && specifiedModel !== GEMINI_MODEL_ALIAS_AUTO) {
if (!isValidModelOrAlias(specifiedModel)) {
const validModels = getValidModelsAndAliases();
throw new FatalConfigError(
`Invalid model: "${specifiedModel}"\n\n` +
`Valid models and aliases:\n${validModels.map((m) => ` - ${m}`).join('\n')}\n\n` +
`Use /model to switch models interactively.`,
);
}
}
const resolvedModel =
specifiedModel === GEMINI_MODEL_ALIAS_AUTO
? defaultModel
@@ -744,11 +713,14 @@ export async function loadCliConfig(
clientVersion: await getVersion(),
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
sandbox: sandboxConfig,
toolSandboxing: settings.security?.toolSandboxing ?? false,
targetDir: cwd,
includeDirectoryTree,
includeDirectories,
loadMemoryFromIncludeDirectories:
settings.context?.loadMemoryFromIncludeDirectories || false,
discoveryMaxDirs: settings.context?.discoveryMaxDirs,
importFormat: settings.context?.importFormat,
debugMode,
question,
@@ -784,6 +756,9 @@ export async function loadCliConfig(
approvalMode,
disableYoloMode:
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
disableAlwaysAllow:
settings.security?.disableAlwaysAllow ||
settings.admin?.secureModeEnabled,
showMemoryUsage: settings.ui?.showMemoryUsage || false,
accessibility: {
...settings.ui?.accessibility,
@@ -823,6 +798,7 @@ export async function loadCliConfig(
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
toolOutputMasking: settings.experimental?.toolOutputMasking,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
@@ -865,7 +841,7 @@ export async function loadCliConfig(
hooks: settings.hooks || {},
disabledHooks: settings.hooksConfig?.disabled || [],
projectHooks: projectHooks || {},
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
onModelChange: (model: string) => saveModelChange(loadSettings(cwd), model),
onReload: async () => {
const refreshedSettings = loadSettings(cwd);
return {
@@ -20,7 +20,12 @@ import {
import { createExtension } from '../test-utils/createExtension.js';
import { ExtensionManager } from './extension-manager.js';
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { GEMINI_DIR, type Config, tmpdir } from '@google/gemini-cli-core';
import {
GEMINI_DIR,
type Config,
tmpdir,
NoopSandboxManager,
} from '@google/gemini-cli-core';
import { createTestMergedSettings, SettingScope } from './settings.js';
describe('ExtensionManager theme loading', () => {
@@ -117,6 +122,7 @@ describe('ExtensionManager theme loading', () => {
terminalHeight: 24,
showColor: false,
pager: 'cat',
sandboxManager: new NoopSandboxManager(),
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],

Some files were not shown because too many files have changed in this diff Show More