Skip to content

[Feat]: Add gemini plugin#912

Open
priyanshuharshbodhi1 wants to merge 6 commits intoComposioHQ:mainfrom
priyanshuharshbodhi1:feat/agent-gemini-plugin
Open

[Feat]: Add gemini plugin#912
priyanshuharshbodhi1 wants to merge 6 commits intoComposioHQ:mainfrom
priyanshuharshbodhi1:feat/agent-gemini-plugin

Conversation

@priyanshuharshbodhi1
Copy link
Copy Markdown

What

Adds @composio/ao-plugin-agent-gemini - full support for Google Gemini CLI as an agent in Agent Orchestrator. Any project can now set agent: gemini in agent-orchestrator.yaml.

Why

Gemini CLI is a first-class AI coding agent alongside Claude Code, Codex, Aider, and OpenCode. This rounds out the default agent roster.

Key Design Decisions

promptDelivery: "post-launch" - Gemini's -p flag exits after one response; AO would mark the session killed. Instead, AO launches Gemini as an interactive REPL in tmux and sends the task prompt after startup via runtime.sendMessage().

Absolute binary resolution - resolveGeminiBinarySync() calls which gemini inside the CLI process (which has nvm in PATH). The resolved absolute path is used in the launch command so tmux shells without nvm still find the real binary.

Hook trust pre-population - AO writes ~/.gemini/trusted_hooks.json when setting up workspace hooks so Gemini skips its "trust these hooks?" warning dialog before the REPL is ready.

Session file path - ~/.gemini/tmp/<sha256(projectRoot)>/chats/session-*.json. Hash is the full 64-char SHA-256 hex, verified against google-gemini/gemini-cli source.

Metadata hooks - AfterTool hook in .gemini/settings.json fires after every run_shell_command. Detects gh pr create, git checkout -b, and gh pr merge to auto-update AO session metadata. Uses a relative path for symlink safety across worktrees.

Activity classification - reads lastUpdated from session JSON (more accurate than file mtime). 30-second session file cache prevents double readdir per poll cycle.

Testing

84 unit tests covering: manifest, binary resolution, launch command flags, activity states, session info/cost extraction, restore command, workspace hooks (write, merge, no-duplicate, hook trust), postLaunchSetup REPL readiness polling.

priyanshuharshbodhi1 and others added 3 commits April 4, 2026 22:49
Add @composio/ao-plugin-agent-gemini workspace package with package.json
and tsconfig.json. Register it in:
- packages/cli/package.json (workspace dependency)
- packages/core/src/plugin-registry.ts (BUILTIN_PLUGINS)
- packages/cli/src/lib/plugins.ts (static import map)
- packages/cli/src/lib/detect-agent.ts (ao init detection list)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full plugin implementation (~450 lines):

- promptDelivery: "post-launch" — Gemini runs as interactive REPL in tmux.
  Using -p causes Gemini to exit after one response (AO marks session killed).
- getLaunchCommand: resolves absolute binary path at creation time so tmux
  shells without nvm in PATH still find the real binary, not a guard wrapper.
- getActivityState: reads lastUpdated from session JSON (more accurate than
  file mtime on network filesystems). 30-second session file cache prevents
  redundant readdir per poll cycle.
- getSessionInfo: extracts summary and per-message token cost from session
  JSON. Falls back to first user message as summary if no .summary field.
- getRestoreCommand: gemini --resume <session-id>, AO sends task post-launch.
- postLaunchSetup: writes workspace hooks, pre-trusts hook in
  trusted_hooks.json, polls tmux pane for REPL prompt (❯) before the
  session manager sends the task — prevents prompt injection into bash.
- detect(): execFileSync("gemini", ["--version"]) binary presence check.

Session path: ~/.gemini/tmp/<sha256(projectRoot)>/chats/session-*.json
Project hash: full 64-char SHA-256 hex, verified against Gemini CLI source.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Coverage:
- manifest shape and plugin export
- getGeminiProjectHash / getGeminiChatsDir path construction
- getLaunchCommand: base, permissionless/auto-edit/default, model, system
  prompt, no -p flag (post-launch delivery)
- getActivityState: active/ready/idle/exited, process check, session cache
- getSessionInfo: summary, cost extraction, first-message fallback
- getRestoreCommand: uses resolved binary + --resume flag
- setupWorkspaceHooks: creates dir, writes script, registers AfterTool hook,
  merges existing settings, no duplicate hooks, pre-trusts in trusted_hooks.json
- postLaunchSetup: writes hooks, polls for ❯ REPL prompt, early exit on guard
  message or tmux failure, uses relative path in settings.json
- METADATA_UPDATER_SCRIPT: bash shebang, run_shell_command filter,
  gh pr create / git checkout / gh pr merge detection, path traversal guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 414b215. Configure here.

* flag — GEMINI.md is the correct way to inject system context.
*/
let pendingSystemPromptFile: string | null = null;
let pendingSystemPrompt: string | null = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shared mutable state causes system prompt race condition

High Severity

pendingSystemPromptFile and pendingSystemPrompt are closure variables on a single Agent instance. The plugin registry calls create() once and shares that instance across all sessions via registry.get(). When two Gemini sessions spawn concurrently, the second getLaunchCommand call overwrites the first session's pending prompt, and the first postLaunchSetup then clears the variables — leaving the second session with no system prompt at all.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 414b215. Configure here.

case "\$AO_DATA_DIR" in
"\$HOME"/.ao/* | "\$HOME"/.agent-orchestrator/* | /tmp/*) ;;
*) echo '{}'; exit 0 ;;
esac
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hook script default AO_DATA_DIR fails its own whitelist

Medium Severity

The bash hook script's default AO_DATA_DIR of $HOME/.ao-sessions doesn't match the whitelist on lines 82–85. The glob "$HOME"/.ao/* requires .ao/ (with a slash), but .ao-sessions starts with .ao- (a hyphen). When AO_DATA_DIR is unset, the script silently exits without updating metadata. The shared agent-workspace-hooks.ts avoids this by using an empty default ("${AO_DATA_DIR:-}").

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 414b215. Configure here.

if (config.issueId) {
env["AO_ISSUE_ID"] = config.issueId;
}
return env;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing buildAgentPath — every non-Claude-Code agent (codex, aider, opencode) prepends ~/.ao/bin to PATH so the AO gh/git wrappers intercept commands and update session metadata. Without this, PRs created by Gemini won't reliably appear in the dashboard.

import { buildAgentPath, setupPathWrapperWorkspace, ... } from "@composio/ao-core";                          
                                                                                                             
// in getEnvironment:                                                                                        
env["PATH"] = buildAgentPath(process.env["PATH"]);                                                           
                                                                                                             
See agent-codex/src/index.ts:389, agent-aider/src/index.ts:149, agent-opencode/src/index.ts:291 for the      
pattern.

},

async setupWorkspaceHooks(workspacePath: string, _config: WorkspaceHooksConfig): Promise<void> {
await writeGeminiHooks(workspacePath);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also needs setupPathWrapperWorkspace(workspacePath) here alongside writeGeminiHooks. This installs the ~/.ao/bin/gh and ~/.ao/bin/git wrappers and writes .ao/AGENTS.md context. Every other PATH-wrapper agent calls both their native hooks AND this function in setupWorkspaceHooks and postLaunchSetup.

async setupWorkspaceHooks(workspacePath: string, _config: WorkspaceHooksConfig): Promise {
await writeGeminiHooks(workspacePath);
await setupPathWrapperWorkspace(workspacePath); // <-- add this
},
Same addition needed in postLaunchSetup at line 773. See agent-codex/src/index.ts:616,633 for the pattern.

try {
let content = pendingSystemPrompt ?? "";
if (pendingSystemPromptFile) {
content = readFileSync(pendingSystemPromptFile, "utf-8");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses readFileSync (sync, blocking) inside an async function. The async readFile from node:fs/promises is already imported at line 23, use that instead:

content = await readFile(pendingSystemPromptFile, "utf-8");

This keeps the async function non-blocking and consistent with the rest of the file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants