Gemini Agent Plugin
The Gemini Agent plugin provides AI-powered coding sessions via the Gemini CLI A2A (Agent-to-Agent) protocol. Each session runs in an isolated container with full developer tooling.
Architecture
How It Works
- User creates a session via the launch form (or shareable URL)
- Backend deploys a container (Cloud Run or Podman) running the Gemini CLI A2A server
- Initial prompt is enqueued as messages in the database
- QueueProcessor (background service) polls for pending messages every 3 seconds
- When the container is ready, QueueProcessor sends messages via A2A JSON-RPC and stores responses
- Frontend polls
/chat-historyevery 2 seconds to display new messages
Backend Modes
The backend supports two container runtime backends via the ContainerBackend interface:
| Mode | Config | Backend | Use Case |
|---|---|---|---|
| Cloud Run | localMode: false (default) | GeminiAgentService | Production, shared environments |
| Local Podman | localMode: true | LocalContainerService | Local development, debugging |
Usage
Creating a Session
Navigate to Gemini Agent in the sidebar and click Launch New Session.
| Field | Required | Description |
|---|---|---|
| Repository | No | GitHub repo in org/name format. Leave empty for no repo. |
| Branch | Yes | Git branch to check out (default: main) |
| Task Description | No | Prompt for the agent. Sent as the initial message. |
| Access Group | No | Restrict session visibility to a team |
| Interactive Mode | No | Keep container alive for follow-up messages (default: on) |
| Session TTL | No | How long to keep the container after last activity (15m–1d, default: 1h) |
| Keep Logs | No | Prevent automatic cleanup of chat history |
When a repository is specified, the backend automatically prepends commands to clone the repo, cd into it, and configure git identity.
Shareable Launch URLs
Sessions can be pre-configured via URL parameters:
/gemini-agent/new?repository=org/repo&branch=main&prompt=Fix+the+bug&auto_launch=true
| Parameter | Description |
|---|---|
repository | GitHub repository |
branch | Branch name |
prompt | Task description |
accessGroup | Access group |
interactive | false to disable interactive mode |
ttl | Session TTL (15m, 30m, 1h, 2h, 4h, 8h, 1d) |
keepLogs | true to keep logs indefinitely |
GITHUB_TOKEN | true to include GitHub token |
GOOGLE_TOKEN | true to include Google token |
auto_launch | true to skip the form and launch immediately |
Session Lifecycle
| Status | Meaning |
|---|---|
creating | Container is being deployed, A2A server starting |
running | Container healthy, accepting messages |
stopped | User stopped the session (container deleted) |
failed | Container failed to start or crashed |
deleted | Cleaned up by backend (displays as "STOPPED" in UI) |
Sending Messages
In the console view, type messages in the input field and press Send (or Enter). Messages are:
- Enqueued in the database (HTTP 202 response)
- Picked up by QueueProcessor within ~3 seconds
- Sent to the A2A container via JSON-RPC
- Response streamed back and stored in database
- Displayed in the chat panel on next poll (~2 seconds)
Messages sent while the agent is processing are queued and processed in order.
Viewing Logs
Click the Logs icon (document icon) in the console header to open the logs drawer. Logs are fetched every 2 seconds. Use the filter input to search log entries.
In local mode, logs come from podman logs. In Cloud Run mode, logs come from Google Cloud Logging.
Credentials
The plugin supports injecting credentials into agent containers:
| Credential | Source | Environment Variable |
|---|---|---|
| GitHub Token | User OAuth (X-GitHub-Token header) | GITHUB_TOKEN |
| Google Token | User OAuth (X-Gcp-Access-Token header) | GOOGLE_ACCESS_TOKEN |
| Gemini API Key | Backstage config or Vault | GEMINI_API_KEY |
| TFC Token | Vault | TFC_TOKEN |
| File Search Auth | Vault override, then user Google OAuth, then backend Gemini API key | FILE_SEARCH_API_KEY or FILE_SEARCH_GOOGLE_ACCESS_TOKEN |
The credentials section in the launch form shows availability status. Credentials are checked via the /sessions/credentials endpoint.
File Search Auth Order
The Gemini agent now injects File Search-specific auth independently of the core Gemini model auth. The runtime resolves File Search auth in this order:
- Vault override secret (
gemini-api-key) - User Google OAuth token
- Backend
geminiAgent.geminiApiKey
When a repository-backed session is launched, the backend also injects:
FILE_SEARCH_STORE_NAMEFILE_SEARCH_REPOFILE_SEARCH_SOURCE_REPOSITORYFILE_SEARCH_AUTH_MODE
API Key vs OAuth Mode
The container supports two authentication modes for the Gemini API:
| Mode | When Used | Behavior |
|---|---|---|
| OAuth | No GEMINI_API_KEY provided | Mounts ~/.gemini/ credential files into container |
| API Key | GEMINI_API_KEY set | Skips credential file mounts, patches entrypoint to disable USE_CCPA |
Container Image
Registry: ghcr.io/badal-io/gemini-cli/gemini-a2a
Source: badal-io/gemini-cli → deploy/cloudrun/
Architectures: linux/amd64, linux/arm64
Image Contents
| Tool | Version | Purpose |
|---|---|---|
| Node.js | 22 (bookworm) | Runtime for Gemini CLI |
| Gemini CLI | monorepo latest | A2A server + CLI |
| gcloud CLI | latest + GKE auth, Cloud Run proxy | GCP operations |
| Terraform | latest | Infrastructure provisioning |
| Vault | latest | Secret management |
| GitHub CLI (gh) | latest | Repository operations |
| Python 3 | system (bookworm) | Scripting |
| bun | latest | Fast JS/TS runtime |
| socat | system | Port forwarding proxy |
| ai-plugin-translator | latest | Gemini extension |
| file-search-extension | workspace package | Repository-scoped Gemini File Search retrieval |
Entrypoint
The entrypoint.sh script:
- Sets environment variables (
USE_CCPA,GEMINI_FOLDER_TRUST,GEMINI_YOLO_MODE) - Starts the A2A server via
node a2a-server.mjson port 8081 - Waits for the
/.well-known/agent-card.jsonendpoint to respond - Forwards external traffic from
0.0.0.0:8080to[::1]:8081via socat (IPv6, since Node.js binds to::1)
File Search Retrieval
The bundled File Search extension gives the agent a repository-scoped retrieval path for documents uploaded by gemini-packager.
Retrieval behavior is intentionally narrow:
- search first to identify likely files or concepts
- fetch targeted file or symbol context second
- expand to adjacent files only when the first retrieval reports partial context
- avoid broad fan-out retrieval on every turn
The extension exposes retrieval tools for:
- searching the configured File Search store
- fetching focused file context
- fetching focused symbol context
- expanding to related files when the current context is incomplete
Configuration
app-config.yaml / app-config.local.yaml
geminiAgent:
localMode: false # true for podman, false for Cloud Run
containerImage: ghcr.io/badal-io/gemini-cli/gemini-a2a:latest
gcpProject: prj-np-devex-backstage-z3brs
gcpRegion: northamerica-northeast1
serviceAccountEmail: gemini-agent@${GCP_PROJECT_ID}.iam.gserviceaccount.com
geminiApiKey: ${GEMINI_API_KEY} # Optional, for API key mode
Local Mode Prerequisites
podmaninstalled andpodman machinerunning~/.gemini/settings.jsonand~/.gemini/oauth_creds.jsonfor Gemini auth (auto-mounted into containers)- On Apple Silicon: image runs under amd64 emulation via
--platform linux/amd64
API Endpoints
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/health | GET | No | Health check ({ status, localMode }) |
/sessions | POST | Yes | Create new session |
/sessions | GET | Yes | List sessions for current user |
/sessions/credentials | GET | Yes | Check available credentials |
/sessions/:id | GET | Yes | Get session status |
/sessions/:id | DELETE | Yes | Stop and delete session |
/sessions/:id | PATCH | Yes | Update session settings |
/sessions/:id/chat-history | GET | Yes | Get chat messages + activity events |
/sessions/:id/message | POST | Yes | Enqueue a message (HTTP 202) |
/sessions/:id/logs | GET | Yes | Batch logs or SSE stream |
/sessions/:id/stream | GET | Yes | SSE keepalive (legacy) |
/sessions/:id/agent-card | GET | Yes | Proxy A2A agent capabilities |
A2A Protocol Integration
The backend communicates with the container's A2A server using JSON-RPC v2.0:
{
"jsonrpc": "2.0",
"method": "message/stream",
"id": "msg-<timestamp>",
"params": {
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "..." }],
"messageId": "<unique-id>"
}
}
}
Message Queue
The QueueProcessor is a background service that manages async message delivery:
- Poll interval: 3 seconds
- Max retries: 10 per message
- Retry cooldown: 10 seconds after failure
- Readiness check: Probes
/.well-known/agent-card.jsonbefore sending - Atomicity: Uses CAS (compare-and-swap) to prevent duplicate processing
Testing
Unit E2E Tests (Mocked)
Located in plugins/gemini-agent/e2e-tests/gemini-agent.spec.ts. These use Playwright with mocked API responses to test UI components without a running backend.
cd backstage
npx playwright test plugins/gemini-agent/e2e-tests/gemini-agent.spec.ts
Integration E2E Tests (Live)
Located in plugins/gemini-agent/e2e-tests/gemini-agent-e2e.spec.ts. These run against a live Backstage instance with real container orchestration.
# Prerequisites: Backstage frontend + backend running (yarn start)
cd backstage
E2E_TEST=true npx playwright test plugins/gemini-agent/e2e-tests/gemini-agent-e2e.spec.ts
The integration test:
- Authenticates via the guest auth provider
- Creates a no-repo session with a random math prompt
- Waits for the container to start and become RUNNING
- Waits for the agent to respond with the correct answer
- Cleans up the session
Environment variables:
| Variable | Default | Description |
|---|---|---|
E2E_TEST | (unset) | Set to true to enable integration tests |
BACKEND_URL | http://localhost:7007 | Backend API base URL |
PLAYWRIGHT_URL | http://localhost:3001 | Frontend URL for browser tests |
Troubleshooting
Common Errors
| Error | Cause | Fix |
|---|---|---|
| "Error: Failed to fetch" on page load | Backend not running | Start both frontend + backend: yarn start |
| "Error: [object Object]" on page load | Auth token rejected by backend | Ensure guest auth provider is enabled in app-config.local.yaml |
PERMISSION_DENIED: run.services.create | Service account lacks IAM role | Grant roles/run.admin to the service account |
UNAUTHENTICATED | No Application Default Credentials | Run gcloud auth application-default login |
| "A2A not ready" (debug log) | Container still starting | Normal — QueueProcessor retries every 3s automatically |
| "No response body from A2A" | Container crashed or network error | Check container logs via the Logs drawer |
| Session stuck in CREATING | Container failed health check | Check podman/Cloud Run logs for startup errors |
| Messages stuck in "queued" | A2A server not responding | Verify container is running; check network connectivity |
Log Messages Reference
Session Creation:
Creating local container gemini-agent-abc123... for user john
Local container gemini-agent-abc123... started, mapped to http://localhost:38920
Enqueued 3 initial message(s) for session abc123-uuid
Message Processing (normal):
Container gemini-agent-abc123... is ready
Message Processing (waiting for container):
QueueProcessor: A2A not ready for session abc123 (probe=503)
This is normal during container startup — the processor retries automatically.
Errors:
QueueProcessor: failed to process message 42: fetch failed
QueueProcessor tick error: ECONNREFUSED
These indicate connectivity problems with the container. Check if the container is still running.
Debugging Local Mode
# Check running containers
podman ps --filter name=gemini-agent
# View container logs
podman logs -f gemini-agent-<session-id-prefix>
# Check container health
curl http://localhost:<mapped-port>/.well-known/agent-card.json
# Restart podman machine (if containers won't start)
podman machine stop && podman machine start
Debugging Cloud Run Mode
# List gemini-agent services
gcloud run services list --filter="metadata.labels.managed-by=backstage" \
--project=<gcp-project> --region=<gcp-region>
# View service logs
gcloud run services logs read gemini-agent-<session-id-prefix> \
--project=<gcp-project> --region=<gcp-region>
# Check service status
gcloud run services describe gemini-agent-<session-id-prefix> \
--project=<gcp-project> --region=<gcp-region>
SSE Streaming Note
Backstage's root HTTP router adds compression() middleware globally, which buffers res.write() chunks. The backend explicitly calls res.flush() after every write in SSE endpoints. If you're extending the plugin with new streaming endpoints, always call (res as any).flush() after each res.write().
Key Files
Backend Plugin (plugins/gemini-agent-backend/src/)
| File | Purpose |
|---|---|
router.ts | Express router — endpoint handlers, auth middleware |
services/types.ts | ContainerBackend interface, Zod schemas, session types |
services/GeminiAgentService.ts | Cloud Run backend (production) |
services/LocalContainerService.ts | Podman backend (local dev) |
services/QueueProcessor.ts | Background message delivery engine |
services/ChatLogStore.ts | Database operations for sessions, messages, events |
services/CredentialService.ts | Credential collection from OAuth, Vault, config |
Frontend Plugin (plugins/gemini-agent/src/)
| File | Purpose |
|---|---|
components/GeminiAgentPage/ | Main page with routing (list, form, console) |
components/LaunchForm/ | Session creation form with credential selection |
components/AgentConsole/ | Console view: chat panel, activity bar, logs |
components/SessionList/ | Session table with status, actions |
api/GeminiAgentClient.ts | REST API client |
api/GeminiAgentApi.ts | API interface and type definitions |
E2E Tests (plugins/gemini-agent/e2e-tests/)
| File | Purpose |
|---|---|
gemini-agent.spec.ts | Unit e2e tests with mocked APIs |
gemini-agent-e2e.spec.ts | Integration e2e tests against live instance |
Database Schema
Sessions Table (gemini_agent_sessions)
| Column | Type | Description |
|---|---|---|
id | UUID | Session identifier |
user_id | string | Backstage user entity ref |
repository | string | GitHub repository |
branch | string | Git branch |
prompt | text | Full prompt (with auto-commands) |
status | string | creating, running, stopped, failed, deleted |
keep_indefinitely | boolean | Prevent automatic cleanup |
interactive_mode | boolean | Keep container alive for follow-ups |
interactive_ttl_minutes | int | TTL after last activity |
last_activity_at | timestamp | Updated on each message |
Message Queue (gemini_agent_message_queue)
| Column | Type | Description |
|---|---|---|
id | int | Auto-increment ID |
session_id | UUID | Parent session |
text | text | Message content |
status | string | pending, processing, completed, failed |
retry_count | int | Number of retry attempts |
Chat Messages (gemini_agent_chat_messages)
| Column | Type | Description |
|---|---|---|
role | string | user or agent |
text | text | Message content |
timestamp | bigint | Unix timestamp |
Activity Events (gemini_agent_activity_events)
| Column | Type | Description |
|---|---|---|
kind | string | thinking-step, status-change, artifact-update |
state | string | submitted, working, thinking, completed, failed |
subject | string | Brief description |
description | text | Detailed description |
Related
- Container image source —
deploy/cloudrun/directory - A2A Protocol — Agent-to-Agent communication standard
- Plugins Overview — All DevEx Platform plugins