feat(agents): First class support for gemini#118
Conversation
📦 Previous Review (superseded by newer review)Pull Request Review: First class support for GeminiThis is a well-architected PR that adds comprehensive Gemini CLI support to Moat. The implementation follows the existing patterns established for Claude Code and demonstrates strong attention to security, credential management, and code quality. StrengthsArchitecture & Design
Implementation Quality
Documentation
Issues & RecommendationsSecurity Concerns1. Token substitution body reading (internal/proxy/proxy.go:358-367) The current implementation reads the entire request body into memory for token substitution: bodyBytes, err := io.ReadAll(req.Body)Issues:
Recommendation: // Limit body size for substitution (e.g., 1MB - auth requests are small)
const maxSubstitutionBodySize = 1 * 1024 * 1024
if req.Body != nil && req.ContentLength > 0 && req.ContentLength < maxSubstitutionBodySize {
bodyBytes, err := io.ReadAll(io.LimitReader(req.Body, maxSubstitutionBodySize))
// ... substitution logic
} else {
// Skip substitution for large/streaming bodies
}This is particularly important because token substitution is only needed for Google's 2. Client credentials in npm package comments (internal/credential/gemini.go:17-37) The OAuth client ID and secret are correctly sourced from the public npm package, but the comments should explicitly note the security implications: // GeminiOAuthClientID is the public OAuth client ID used by Gemini CLI.
//
// Security note: This is an "installed application" OAuth client per Google's model.
// The client secret is not actually secret (it's in the public npm package).
// Real security comes from Google's OAuth flow (user must authenticate via browser)
// and the fact that refresh tokens are user-specific and never shared.This helps future maintainers understand why hardcoding these values is acceptable. Code Quality Issues3. Duplicate logic in GeminiSetup.populateStagingDir (internal/gemini/provider.go:104-139) The OAuth placeholder token logic duplicates the 4. Magic number in refresh timing (internal/gemini/credential_refresh.go:14) refreshBuffer = 5 * time.MinuteThe choice should be documented: Google OAuth access tokens typically have 1 hour lifetime. We refresh 5 minutes early to ensure tokens don't expire during active requests. 5. Missing context cancellation in refreshLoop (internal/gemini/credential_refresh.go:64-109) The Documentation Gaps6. Missing network access documentation The PR adds 7. gemini.sync_logs field undocumented behavior The Minor Issues8. Inconsistent error formatting Some errors use sentence case with periods, others don't. Pick one style and apply consistently (Go stdlib uses sentence case, no period). 9. Test coverage gap for error paths
10. Proxy credential map growth The proxy stores credentials in unbounded maps. For long-running processes with many runs, this could grow. From my reading, proxy instances are per-run and short-lived, so this is likely fine. A comment clarifying the lifecycle would help. Testing Recommendations11. E2E test for Gemini flow Consider adding an E2E test similar to the existing ones in SummaryThis is high-quality work. The OAuth token refresh architecture is particularly well-done and sets a good pattern for future providers. The security model is sound (token substitution is clever), and the code is well-tested. Recommended actions before merge:
Nice-to-haves:
The other issues are minor polish items that can be addressed in follow-up PRs. Recommendation: Approve with minor changes requested (issues #1 and #5 should be fixed before merge). Great work on this feature! The architecture is solid and extensible. |
Pull Request Review: First-class Gemini CLI SupportI've completed a thorough code review of this PR. Overall, this is a well-architected implementation with excellent security practices. The token substitution mechanism is particularly clever and effectively prevents credential exposure. Below are my findings organized by category. ✅ Strengths1. Excellent Security ModelThe credential handling is exemplary:
2. Clean Architecture - RefreshableProvider Interface// internal/credential/provider.go:161-173
type RefreshableProvider interface {
StartRefresh(p ProxyConfigurer, cred *Credential) io.Closer
}
3. Robust Token Refresh Loop
4. Proper Resource Cleanup
5. Good Test Coverage
|
| Category | Rating | Notes |
|---|---|---|
| Security | ✅ Excellent | No credential exposure, proper token isolation |
| Architecture | ✅ Excellent | Clean interfaces, zero provider-specific coupling |
| Error Handling | Minor fragility with string matching | |
| Resource Management | ✅ Excellent | Proper cleanup, no leaks |
| Test Coverage | Most paths covered, some edge cases missing | |
| Documentation | Core logic documented, minor gaps |
🎯 Recommendations
Must Fix (before merge)
None - no blocking issues found
Should Fix (high value, low effort)
- Create typed error for OAuth failures (described in Issue feat(routing): add hostname-based service routing for agents #1 above)
- Improves reliability of revocation detection
- Makes error handling more maintainable
- ~30 lines of code
Nice to Have (future improvements)
- Add test for token revocation path (described in Issue Add Claude Code GitHub Workflow #2)
- Add documentation comments for empty
Cleanup()method and timer drain pattern
✅ Approval
This is a high-quality implementation that follows Go best practices and maintains excellent security hygiene. The architecture is extensible (any future provider can implement RefreshableProvider), the code is clean and well-tested, and the security model is sound.
The identified issues are minor and can be addressed in follow-up PRs without blocking this merge.
Recommendation: APPROVE ✅
Great work on this feature! The token substitution mechanism is particularly elegant and the RefreshableProvider interface is a clean abstraction that will serve future providers well.
Add a complete Gemini CLI provider following the established provider framework pattern (Claude, Codex). This enables running Google's Gemini CLI in isolated containers with transparent credential injection. Key additions: - Gemini provider package (internal/providers/gemini/) implementing CredentialProvider and AgentProvider interfaces - Dual auth support: OAuth (imported from Gemini CLI) and API key - Proxy token substitution for OAuth (cloudcode-pa.googleapis.com) and API key injection (generativelanguage.googleapis.com) - Background OAuth token refresh with exponential backoff - Container staging directory for settings.json and oauth_creds.json - MCP server configuration generation for Gemini - Session management for tracking Gemini sessions - CLI commands: `moat gemini` and `moat gemini sessions` - `moat grant gemini` with OAuth import and API key flows - Container init script support (MOAT_GEMINI_INIT) - gemini-cli added to dependency registry as npm package - Documentation: guide, CLI reference, agent.yaml reference - Example agent.yaml configuration
Previously, if Google's token endpoint returned a non-200 response with a non-JSON body, the unmarshal error was silently discarded and an empty OAuthError was returned. Now we return a descriptive error with the HTTP status code and raw response body, making failures easier to diagnose. Addresses PR #118 code review feedback.
Summary
moat geminicommand for running Google Gemini CLI in isolated containersmoat grant geminiwith two auth methods: import existing Gemini CLI OAuth credentials, or API keyRefreshableProviderinterface (generic, not Gemini-specific)Design decisions
OAuth uses a different API backend than API key mode. Gemini CLI routes to
cloudcode-pa.googleapis.com(Cloud Code PrivateAPI) for OAuth and
generativelanguage.googleapis.com(Google AI SDK) for API keys. These are different APIs with differentcontracts. The proxy injects credentials for the correct host based on auth type.
Token substitution for startup validation. Gemini CLI validates OAuth tokens at startup by POSTing to
oauth2.googleapis.com/tokeninfo. The container has placeholder tokens in~/.gemini/oauth_creds.json— the proxy replacesplaceholders with real tokens in the Authorization header and request body before forwarding. Google validates the real token and
returns a real response. This avoids both faking responses and putting real tokens in the container.
RefreshableProviderinterface. Background token refresh is opt-in via an interface onProviderSetup. The core run lifecycle(
manager.go,run.go) has zero provider-specific code — it checks for the interface and managesio.Closerinstancesgenerically. Any future provider with expiring tokens can implement this.
oauth_creds.jsonwith far-future expiry. The placeholder credentials file hasexpiry_dateset to 1 year in the future soGemini CLI doesn't attempt its own token refresh (which would fail on placeholder refresh tokens). The proxy handles real expiry
and refresh on the host side.
New proxy capabilities
RemoveRequestHeader(host, header)— strips client-sent headers that conflict with injected credentialsSetTokenSubstitution(host, placeholder, realToken)— replaces placeholder strings in request headers and bodies beforeforwarding
Files
New packages:
internal/gemini/— Provider setup, credential refresh, session management, MCP config generation, settings typesNew CLI:
cmd/moat/cli/gemini.go—moat geminicommand andmoat gemini sessionssubcommandcmd/moat/cli/gemini_run.go— Run logic, dependency injection, network allow listModified:
internal/credential/provider.go—ProxyConfigurerinterface gainsRemoveRequestHeaderandSetTokenSubstitution; newRefreshableProviderinterfaceinternal/proxy/proxy.go— Token substitution, header removal, both HTTP and CONNECT pathsinternal/run/manager.go— Generic credential refresh lifecycle viaRefreshableProviderinternal/run/run.go—credentialClosers []io.Closerreplaces provider-specific fieldsDocs:
docs/content/guides/08-running-gemini.md— Full user guidedocs/content/concepts/02-credentials.md— Gemini credential sectiondocs/content/reference/01-cli.md—moat geminiandmoat grant geminireferencedocs/content/reference/02-agent-yaml.md—gemini.sync_logsandgemini.mcpfields