mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(providers): Gemini CLI provider — ModelProvider.GEMINI Mirrors the grok blueprint with source-verified divergences (all facts pinned against google-gemini/gemini-cli @ 9681621c): no refresher daemon — Google's refresh tokens are reusable, so the RO host mount is COPIED to a writable container-local ~/.gemini and each container refreshes in-process independently (the write-back crash risk on RO never triggers); settings.json renders security.auth.selectedType 'oauth-personal', experimental.enableAgents=false (subagent ban), autoConfigureMemory=false with a bounded heap; tool scoping rides the tiered TOML Policy Engine (deny-only rules that yolo mode structurally cannot beat); gemini -p with --output-format stream-json; usage parsed from the run's own stdout stats — the adversarial pass caught the parser reading the json-mode nested shape while the entrypoint runs stream-json's FLAT shape (every real run would have priced $0 forever, hidden by fixtures sharing the assumption) — now flat-primary with the nested shape as cited fallback; rate-limit classified from structured error.type only (model-echo immune), native exit 41 auth passthrough; per-model pricing for the three GA models; migrations 084 (enum) + 085 (seed) complete the 082-085 finale chain. V1 excludes interactive intake/secretary. Stack-merge required two behavior-preserving complexity refactors in the shared park/usage plumbing (a park-pair loop; a usage-reader dispatch dict). * fix(providers): route gemini usage read through the containment barrier Mirrors the codex/grok fix — _gemini_usage_json now delegates to _read_usage_json_contained, so CodeQL's path-injection alert on the gemini read is resolved by the same resolve-and-contain guard. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
126 lines
6.7 KiB
Bash
126 lines
6.7 KiB
Bash
#!/usr/bin/env bash
|
|
# Entrypoint for the roboco-agent-gemini image (one-shot delivery roles).
|
|
#
|
|
# Runs an agent on Google's official `gemini` CLI, authenticated by an OAuth
|
|
# login via a mounted ~/.gemini/oauth_creds.json — the parity analogue of the
|
|
# Claude Code path's mounted ~/.claude and the grok path's mounted ~/.grok. The
|
|
# gateway, identity, and workspace are mounted by the orchestrator's shared
|
|
# container assembly (the same that wires Claude/grok); this entrypoint copies
|
|
# the staged OAuth credential into a writable ~/.gemini, renders the gemini
|
|
# runtime config from that mount, and runs the CLI headless.
|
|
set -euo pipefail
|
|
|
|
# The orchestrator mounts the host ~/.gemini DIRECTORY read-only at this
|
|
# staging path (roboco.llm.providers.gemini._append_gemini_auth_mount). Copy it
|
|
# into the image's own ~/.gemini (agent-owned, writable — see the Dockerfile)
|
|
# so the CLI's in-process OAuth refresh (google-auth-library) can write the
|
|
# refreshed token back locally: Google's refresh token is REUSABLE, so each
|
|
# container refreshing its OWN copy independently is safe (contrast grok's
|
|
# live-symlinked RO mount, which needs single-writer orchestrator-side
|
|
# serialization because xAI's refresh token is single-use — see
|
|
# roboco.llm.providers.gemini's module docstring). The host's copy is never
|
|
# touched.
|
|
AUTH_STAGING_DIR="/home/agent/.gemini-auth-ro"
|
|
if [ -d "$AUTH_STAGING_DIR" ]; then
|
|
cp -a "$AUTH_STAGING_DIR"/. /home/agent/.gemini/ 2>/dev/null || true
|
|
fi
|
|
|
|
# Auth preflight. Without a real OAuth credential the CLI would hang at an
|
|
# interactive consent prompt in a headless container (or refuse outright,
|
|
# depending on the auth-check path) — refuse fast instead: exit 41 (the CLI's
|
|
# own dedicated auth-failure code, so the orchestrator's exit classifier
|
|
# treats a missing credential identically to a real CLI auth rejection).
|
|
if [ ! -s /home/agent/.gemini/oauth_creds.json ]; then
|
|
echo "[gemini] OAuth credential missing at ~/.gemini/oauth_creds.json — refusing" \
|
|
"to run. Run 'gemini' interactively once on the host (or set" \
|
|
"ROBOCO_HOST_GEMINI_DIR at the directory holding oauth_creds.json) before" \
|
|
"spawning Gemini agents." >&2
|
|
exit 41
|
|
fi
|
|
|
|
# Render ~/.gemini/settings.json (mcpServers + selectedType/enableAgents/
|
|
# autoConfigureMemory) + GEMINI.md + the Policy Engine TOML. Run from /app so
|
|
# `python -m` resolves the INSTALLED roboco package: dev/doc/qa agents run at
|
|
# their workspace-clone cwd, whose own roboco/ dir would shadow it on the
|
|
# sys.path front (the ModuleNotFound lesson). The render reads
|
|
# ROBOCO_MCP_CONFIG + ROBOCO_AGENT_ID and writes the config files.
|
|
( cd /app && python -m roboco.llm.providers.gemini_cli_config )
|
|
|
|
# Prompt-injection guard (parity with the Claude UserPromptSubmit hook / the
|
|
# grok path): the task prompt is DATA, not instructions — refuse a poisoned
|
|
# one before the model sees it. Same patterns as
|
|
# docker/scripts/user-prompt-hook.sh; run from /app too.
|
|
if ! ( cd /app && python -m roboco.agent_sdk.prompt_guard "${ROBOCO_INITIAL_PROMPT:-}" ); then
|
|
echo "Refusing to run: task prompt matched a prompt-injection pattern." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# MCP-dead-server mitigation. The Gemini CLI has no native fail-fast for a
|
|
# dead MCP server (a disconnected server just logs DISCONNECTED and the run
|
|
# continues, tool-less) — so this cheap out-of-band check (the same
|
|
# gateway-venv import probe the orchestrator's reaper uses,
|
|
# `_probe_gateway_health`) catches a corrupted /app/.venv BEFORE the run
|
|
# starts, exiting 52 (EX_CONFIG-style: config/environment broken, not a task
|
|
# failure) instead of burning a whole run against a tool-less gateway.
|
|
if ! /app/.venv/bin/python -c "import httpx, mcp" 2>/dev/null; then
|
|
echo "[gemini] MCP gateway venv is broken (httpx/mcp import failed) — refusing" \
|
|
"to run against a dead gateway." >&2
|
|
exit 52
|
|
fi
|
|
|
|
# Run the agent. The prompt comes from an env var (never an untrusted argv
|
|
# positional). `< /dev/null` keeps the headless run from blocking on stdin. We
|
|
# do NOT `exec`: the script regains control to classify the exit code +
|
|
# capture usage. `--output-format stream-json` + `tee` streams the run to the
|
|
# container's stdout LIVE (so `docker logs` shows the agent reasoning /
|
|
# answering in real time, parity with the Claude/grok paths' stream-json)
|
|
# while ALSO capturing it to RUN_LOG for the usage-stats / exit-classification
|
|
# reads below. stderr goes to ERR_LOG and is surfaced after the run.
|
|
RUN_LOG="/tmp/gemini-run.json"
|
|
ERR_LOG="/tmp/gemini-run.err"
|
|
# gemini_cli_config (rendered above) already wrote the per-role CLI flag
|
|
# tokens (today: just --approval-mode yolo — tool scoping lives in
|
|
# settings.json/policy TOML, not CLI flags) one per line to this file.
|
|
GEMINI_ARGS_FILE="${ROBOCO_GEMINI_ARGS_FILE:-/tmp/roboco-gemini-args}"
|
|
mapfile -t GEMINI_ARGS < "$GEMINI_ARGS_FILE"
|
|
|
|
set +e
|
|
gemini -p "${ROBOCO_INITIAL_PROMPT:-}" \
|
|
-m "${ROBOCO_AGENT_MODEL:-gemini-2.5-pro}" \
|
|
--output-format stream-json \
|
|
"${GEMINI_ARGS[@]}" \
|
|
< /dev/null 2> "$ERR_LOG" | tee "$RUN_LOG"
|
|
run_rc=${PIPESTATUS[0]}
|
|
set -e
|
|
# stdout already streamed live via tee; surface stderr (tool calls / errors) too.
|
|
[ -s "$ERR_LOG" ] && cat "$ERR_LOG" >&2
|
|
|
|
# Capture token usage from the run's own captured stdout (no session-file
|
|
# scraping — Gemini reports per-model stats directly, unlike grok). Writes a
|
|
# usage.json the orchestrator reads back at finalize. Best-effort; never fails
|
|
# the run. Run from /app for the same module-resolution reason as the render
|
|
# above.
|
|
( cd /app && ROBOCO_GEMINI_RUN_LOG="$RUN_LOG" \
|
|
python -m roboco.llm.providers.gemini_cli_usage ) || true
|
|
|
|
# Exit-code classification. 41 (auth) is the CLI's own dedicated exit code and
|
|
# passes through unchanged. A quota/rate-limit error has NO dedicated CLI exit
|
|
# code — it falls to the CLI's generic 1 — so this remaps it to 75 (EX_TEMPFAIL)
|
|
# by parsing the run's captured JSON for a quota-error `error.type`
|
|
# (TerminalQuotaError / RetryableQuotaError), the parity analogue of grok's
|
|
# text-grep exit-75 detector. The orchestrator parks the GEMINI provider on 75
|
|
# instead of the dispatcher respawning the same task every tick.
|
|
classified_rc=$(cd /app && ROBOCO_GEMINI_RUN_LOG="$RUN_LOG" \
|
|
ROBOCO_GEMINI_CLI_EXIT_CODE="$run_rc" \
|
|
python -m roboco.llm.providers.gemini_cli_usage --classify-exit)
|
|
if [ "$classified_rc" != "$run_rc" ]; then
|
|
echo "[gemini] exit $run_rc reclassified to $classified_rc (quota/rate-limit" \
|
|
"detected in the run output) — the orchestrator parks the provider; the" \
|
|
"task is retried when the limit lifts." >&2
|
|
fi
|
|
|
|
# A graceful exit without a terminal verb is handled server-side by the
|
|
# orchestrator (_handle_stopped_container substitutes the still-owned task) —
|
|
# the gemini-cli runtime needs no in-container SDK server for that.
|
|
exit "$classified_rc"
|