From ef13c9bc3d17c8f7bbe0d7717aee105fdf44b789 Mon Sep 17 00:00:00 2001 From: Ramesh N Date: Mon, 20 Apr 2026 23:26:36 +0530 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20gemini=20agent=20=E2=80=94=20setting?= =?UTF-8?q?s=20validation,=20parser=20crash,=20and=20exit=20code=20inferen?= =?UTF-8?q?ce=20(#463)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: gemini agent — settings validation, parser crash, and exit code inference Three bugs preventing Gemini tasks from running end-to-end: - settings.json rejected "yolo" as defaultApprovalMode (valid values are 'default' | 'auto_edit' | 'plan'); mapped to "auto_edit" since yolo mode is still applied via the --approval-mode CLI flag - tool_result events with undefined output crashed the parser because JSON.stringify(undefined) returns undefined rather than a string - inferExitCode matched the bare string "GEMINI_API_KEY" as an auth error, causing false positives; now requires error-descriptive context words Also adds gemini and openclaw to AgentTypeSchema which was missing them. Co-Authored-By: Claude Sonnet 4.6 * test(agent-adapters): update gemini settings approval mode assertion settings.json general.defaultApprovalMode requires "default" | "auto_edit" | "plan". "yolo" is not a valid enum value — it can only be enabled via CLI flag (--yolo or --approval-mode=yolo). See: https://geminicli.com/docs/reference/configuration/ Update the test expectation to reflect that the adapter maps "yolo" → "auto_edit" for the settings file while keeping the CLI flag as-is. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Ramesh Nethi Co-authored-by: Claude Sonnet 4.6 --- apps/api/src/schemas/task.ts | 2 +- apps/api/src/services/gemini-event-parser.ts | 3 ++- apps/api/src/workers/task-worker.ts | 13 ++++++++----- packages/agent-adapters/src/gemini.test.ts | 4 +++- packages/agent-adapters/src/gemini.ts | 5 ++++- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/api/src/schemas/task.ts b/apps/api/src/schemas/task.ts index d55349b6..c857d130 100644 --- a/apps/api/src/schemas/task.ts +++ b/apps/api/src/schemas/task.ts @@ -35,7 +35,7 @@ export const TaskActivitySubstateSchema = z .describe("Activity sub-state for running tasks"); export const AgentTypeSchema = z - .enum(["claude-code", "codex", "copilot", "opencode"]) + .enum(["claude-code", "codex", "copilot", "opencode", "gemini", "openclaw"]) .describe("Agent runtime that executes the task"); export const WorktreeStateSchema = z diff --git a/apps/api/src/services/gemini-event-parser.ts b/apps/api/src/services/gemini-event-parser.ts index 6a6db769..e9c13929 100644 --- a/apps/api/src/services/gemini-event-parser.ts +++ b/apps/api/src/services/gemini-event-parser.ts @@ -115,7 +115,8 @@ export function parseGeminiEvent( // Tool result if (event.type === "tool_result") { - const output = typeof event.output === "string" ? event.output : JSON.stringify(event.output); + const output = + typeof event.output === "string" ? event.output : (JSON.stringify(event.output) ?? ""); const trimmed = output.length > 300 ? output.slice(0, 300) + "\u2026" : output; if (trimmed.trim()) { entries.push({ diff --git a/apps/api/src/workers/task-worker.ts b/apps/api/src/workers/task-worker.ts index ed3e5475..33e4dc71 100644 --- a/apps/api/src/workers/task-worker.ts +++ b/apps/api/src/workers/task-worker.ts @@ -1703,7 +1703,7 @@ export function buildAgentCommand( ? ` -m ${JSON.stringify(env.OPTIO_GEMINI_MODEL)}` : ""; return [ - `echo "[optio] Running Google Gemini${opts?.isReview ? " (review)" : ""}..."`, + `echo "[optio] Running Gemini..."`, `gemini -p "$OPTIO_PROMPT" \\`, ` --output-format stream-json \\`, ` --approval-mode yolo${geminiModelFlag}`, @@ -1796,12 +1796,15 @@ export function inferExitCode(agentType: string, logs: string): number { } case "gemini": { const hasErrorEvent = logs.includes('"type":"error"') || logs.includes('"type": "error"'); - const hasAuthError = /GEMINI_API_KEY|GOOGLE_API_KEY|permission denied|unauthorized/i.test( - logs, - ); + // Match auth errors by error-descriptive patterns only (not bare env var names which + // could appear in diagnostic output and cause false positives). + const hasAuthError = + /api.?key.*(?:invalid|not valid|missing)|invalid.*api.?key|api_key_invalid|permission denied|unauthorized/i.test( + logs, + ); const hasQuotaError = /quota|resource.?exhausted|rate.?limit/i.test(logs); const hasModelError = /model.*not found|model_not_found|does not exist.*model/i.test(logs); - const hasTurnLimit = /turn.?limit|exit code 53/i.test(logs); + const hasTurnLimit = /turn.?limit|exit:?\s*53\b/i.test(logs); return hasErrorEvent || hasAuthError || hasQuotaError || hasModelError || hasTurnLimit ? 1 : 0; diff --git a/packages/agent-adapters/src/gemini.test.ts b/packages/agent-adapters/src/gemini.test.ts index ccbd42f3..6ebd568c 100644 --- a/packages/agent-adapters/src/gemini.test.ts +++ b/packages/agent-adapters/src/gemini.test.ts @@ -96,7 +96,9 @@ describe("GeminiAdapter", () => { expect(settingsFile).toBeDefined(); const settings = JSON.parse(settingsFile!.content); expect(settings.security.auth.selectedType).toBe("gemini-api-key"); - expect(settings.general.defaultApprovalMode).toBe("yolo"); + // "yolo" is not a valid settings.json enum; it's applied via the CLI --approval-mode flag. + // Settings file uses "auto_edit" as the nearest valid equivalent. + expect(settings.general.defaultApprovalMode).toBe("auto_edit"); expect(settings.telemetry.enabled).toBe(false); }); diff --git a/packages/agent-adapters/src/gemini.ts b/packages/agent-adapters/src/gemini.ts index 36bdc952..22b0e22b 100644 --- a/packages/agent-adapters/src/gemini.ts +++ b/packages/agent-adapters/src/gemini.ts @@ -100,10 +100,13 @@ export class GeminiAdapter implements AgentAdapter { const approvalMode = input.geminiApprovalMode ?? "yolo"; const authType = input.geminiAuthMode === "vertex-ai" ? "vertex-ai" : "gemini-api-key"; const maxSessionTurns = input.maxTurnsCoding ?? 250; + // "yolo" is not a valid settings.json enum (valid: 'default' | 'auto_edit' | 'plan'). + // Yolo mode is applied via the --approval-mode CLI flag; settings.json uses "auto_edit". + const settingsApprovalMode = approvalMode === "yolo" ? "auto_edit" : approvalMode; const geminiSettings = { security: { auth: { selectedType: authType } }, model: { maxSessionTurns }, - general: { defaultApprovalMode: approvalMode }, + general: { defaultApprovalMode: settingsApprovalMode }, telemetry: { enabled: false }, }; setupFiles.push({ From 80c8e5c9a3032b79d491bbdbee50ae4c73ece787 Mon Sep 17 00:00:00 2001 From: Ramesh N Date: Mon, 20 Apr 2026 23:26:45 +0530 Subject: [PATCH 2/4] feat(helm): add GKE & Gateway deployment enhancements (#461) - Fix kubeVersion constraint to allow GKE pre-release versions (>=1.33.0-0) - Add WebSocket appProtocol annotation to API service for proper Gateway routing - Support existing Gateway via configurable parentRefs in HTTPRoute - Add --platform flag to image build script for multi-architecture builds These changes enable deployment on GKE clusters with Gateway API and improve cross-platform image building for ARM/AMD64 architectures. Co-authored-by: Ramesh Nethi Co-authored-by: Claude Sonnet 4.5 --- helm/optio/Chart.yaml | 2 +- helm/optio/templates/api-deployment.yaml | 1 + helm/optio/templates/gateway.yaml | 2 +- helm/optio/templates/httproute.yaml | 4 +++ images/build.sh | 42 +++++++++++++++++++----- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/helm/optio/Chart.yaml b/helm/optio/Chart.yaml index e5e935ab..318ed306 100644 --- a/helm/optio/Chart.yaml +++ b/helm/optio/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: optio description: AI Agent Workflow Orchestration -kubeVersion: ">=1.33.0" +kubeVersion: ">=1.33.0-0" type: application version: 0.1.0 appVersion: "0.1.0" diff --git a/helm/optio/templates/api-deployment.yaml b/helm/optio/templates/api-deployment.yaml index ec244661..64ecb9f8 100644 --- a/helm/optio/templates/api-deployment.yaml +++ b/helm/optio/templates/api-deployment.yaml @@ -160,6 +160,7 @@ spec: ports: - port: {{ .Values.api.port }} targetPort: {{ .Values.api.port }} + appProtocol: kubernetes.io/ws # WebSocket support for /ws/* endpoints {{- if and (eq .Values.api.service.type "NodePort") .Values.api.service.nodePort }} nodePort: {{ .Values.api.service.nodePort }} {{- end }} diff --git a/helm/optio/templates/gateway.yaml b/helm/optio/templates/gateway.yaml index 58c938c9..9253e3d0 100644 --- a/helm/optio/templates/gateway.yaml +++ b/helm/optio/templates/gateway.yaml @@ -1,5 +1,5 @@ {{- include "optio.validateNetworking" . }} -{{- if and .Values.gatewayAPI.enabled (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1") }} +{{- if and .Values.gatewayAPI.enabled (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1") (not .Values.gatewayAPI.parentRefs) }} apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: diff --git a/helm/optio/templates/httproute.yaml b/helm/optio/templates/httproute.yaml index b2760fe8..221f5978 100644 --- a/helm/optio/templates/httproute.yaml +++ b/helm/optio/templates/httproute.yaml @@ -13,8 +13,12 @@ metadata: {{- end }} spec: parentRefs: + {{- if .Values.gatewayAPI.parentRefs }} + {{- toYaml .Values.gatewayAPI.parentRefs | nindent 4 }} + {{- else }} - name: {{ .Release.Name }}-gateway namespace: {{ .Values.namespace }} + {{- end }} {{- if .Values.gatewayAPI.hostname }} hostnames: - {{ .Values.gatewayAPI.hostname | quote }} diff --git a/images/build.sh b/images/build.sh index 981d3f99..5b77b4bd 100755 --- a/images/build.sh +++ b/images/build.sh @@ -3,40 +3,66 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Parse arguments TAG="${1:-latest}" +PLATFORM="" + +# Check for --platform flag in any position +i=0 +next_is_platform=false +for arg in "$@"; do + i=$((i + 1)) + if [[ "$next_is_platform" == true ]]; then + PLATFORM="$arg" + next_is_platform=false + elif [[ "$arg" == --platform=* ]]; then + PLATFORM="${arg#*=}" + elif [[ "$arg" == "--platform" ]]; then + next_is_platform=true + fi +done + +# Build platform flag if specified +PLATFORM_FLAG="" +if [ -n "${PLATFORM}" ]; then + PLATFORM_FLAG="--platform ${PLATFORM}" + echo "Building for platform: ${PLATFORM}" +fi echo "=== Building Optio Agent Images ===" +echo "Tag: ${TAG}" # Base image (all others depend on this) echo "[1/8] Building optio-base..." -docker build -t "optio-base:${TAG}" -f "${SCRIPT_DIR}/base.Dockerfile" "${ROOT_DIR}" +docker build ${PLATFORM_FLAG} -t "optio-base:${TAG}" -f "${SCRIPT_DIR}/base.Dockerfile" "${ROOT_DIR}" echo "Using base internal image: optio-base:${TAG}" BASE_IMAGE="optio-base:${TAG}" # Language-specific images (can be built in parallel) echo "[2/8] Building optio-node..." -docker build -t "optio-node:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/node.Dockerfile" "${ROOT_DIR}" & +docker build ${PLATFORM_FLAG} -t "optio-node:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/node.Dockerfile" "${ROOT_DIR}" & echo "[3/8] Building optio-python..." -docker build -t "optio-python:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/python.Dockerfile" "${ROOT_DIR}" & +docker build ${PLATFORM_FLAG} -t "optio-python:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/python.Dockerfile" "${ROOT_DIR}" & echo "[4/8] Building optio-go..." -docker build -t "optio-go:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/go.Dockerfile" "${ROOT_DIR}" & +docker build ${PLATFORM_FLAG} -t "optio-go:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/go.Dockerfile" "${ROOT_DIR}" & echo "[5/8] Building optio-rust..." -docker build -t "optio-rust:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/rust.Dockerfile" "${ROOT_DIR}" & +docker build ${PLATFORM_FLAG} -t "optio-rust:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/rust.Dockerfile" "${ROOT_DIR}" & echo "[6/8] Building optio-dind..." -docker build -t "optio-dind:${TAG}" -f "${SCRIPT_DIR}/dind.Dockerfile" "${ROOT_DIR}" & +docker build ${PLATFORM_FLAG} -t "optio-dind:${TAG}" -f "${SCRIPT_DIR}/dind.Dockerfile" "${ROOT_DIR}" & echo "[7/8] Building optio-optio (operations assistant)..." -docker build -t "optio-optio:${TAG}" -f "${ROOT_DIR}/Dockerfile.optio" "${ROOT_DIR}" & +docker build ${PLATFORM_FLAG} -t "optio-optio:${TAG}" -f "${ROOT_DIR}/Dockerfile.optio" "${ROOT_DIR}" & wait echo "[8/8] Building optio-full..." -docker build -t "optio-full:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/full.Dockerfile" "${ROOT_DIR}" +docker build ${PLATFORM_FLAG} -t "optio-full:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/full.Dockerfile" "${ROOT_DIR}" # Tag optio-base as the default docker tag "optio-base:${TAG}" "optio-agent:${TAG}" From 8d2e082278b644bd12e0a9db74a74e4f2d6193f3 Mon Sep 17 00:00:00 2001 From: Ramesh N Date: Mon, 20 Apr 2026 23:26:54 +0530 Subject: [PATCH 3/4] fix: correct sub-hour timezone drift in getETDate (#462) The two-pass drift correction used setHours() (local time) and only accounted for whole-hour drift. On systems with sub-hour UTC offsets (e.g. IST = UTC+5:30) this introduced a 30-minute error, causing off-peak transition times to be calculated incorrectly. Fix: use setTime() with a millisecond-precision drift that includes the minute component from getETComponents. Co-authored-by: Ramesh Nethi Co-authored-by: Claude Sonnet 4.6 --- packages/shared/src/utils/off-peak.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/utils/off-peak.ts b/packages/shared/src/utils/off-peak.ts index e383ac2d..284e3388 100644 --- a/packages/shared/src/utils/off-peak.ts +++ b/packages/shared/src/utils/off-peak.ts @@ -135,10 +135,11 @@ function getETDate(ref: Date, targetHourET: number): Date { `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${String(targetHourET).padStart(2, "0")}:00:00`, ); - // Get what hour ET thinks this is - const { hour: guessHour } = getETComponents(guess); - const drift = guessHour - targetHourET; - guess.setHours(guess.getHours() - drift); + // Get what ET thinks this is, including minutes (sub-hour timezone offsets like IST = UTC+5:30 + // would otherwise introduce a 30-minute drift when using setHours in local time). + const { hour: guessHour, minute: guessMinute } = getETComponents(guess); + const driftMs = (guessHour - targetHourET) * 60 * 60 * 1000 + guessMinute * 60 * 1000; + guess.setTime(guess.getTime() - driftMs); return guess; } From 8a7db3276f5f9cdf7b73eb618199d3691f374688 Mon Sep 17 00:00:00 2001 From: Ramesh N Date: Mon, 20 Apr 2026 23:41:15 +0530 Subject: [PATCH 4/4] fix(images): change agent user UID from 1000 to 1001 (#466) The K8s securityContext in k8s-workload-service.ts sets runAsUser=1001, but the base image was creating the agent user with UID 1000. This mismatch caused git to fail with permission errors because: - Pod runs as UID 1001 (no matching user in /etc/passwd) - No HOME directory set for UID 1001 - Git falls back to / for .gitconfig - Permission denied: /.gitconfig Fix: - Changed agent user from UID 1000 to UID 1001 - Removed ubuntu user deletion (no longer needed - agent naturally gets 1001) - Simplified Dockerfile by removing unnecessary steps This aligns with commit eeaa4ba which changed K8s to UID 1001. Co-authored-by: Ramesh Nethi Co-authored-by: Claude Sonnet 4.5 --- images/base.Dockerfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/images/base.Dockerfile b/images/base.Dockerfile index f2f5c767..098fb5de 100644 --- a/images/base.Dockerfile +++ b/images/base.Dockerfile @@ -69,11 +69,9 @@ COPY scripts/optio-gh-wrapper /usr/local/bin/optio-gh-wrapper COPY scripts/optio-glab-wrapper /usr/local/bin/optio-glab-wrapper RUN chmod +x /usr/local/bin/optio-git-credential /usr/local/bin/optio-gh-wrapper /usr/local/bin/optio-glab-wrapper -# Non-root user (UID 1000 to match k8s securityContext) -# Ubuntu 24.04 ships with 'ubuntu' user at UID 1000 — remove it first -RUN userdel -r ubuntu 2>/dev/null || true \ - && groupadd -g 1000 agent \ - && useradd -m -s /bin/bash -u 1000 -g 1000 agent \ +# Non-root user (UID 1001 to match k8s securityContext) +RUN groupadd -g 1001 agent \ + && useradd -m -s /bin/bash -u 1001 -g 1001 agent \ && chown -R agent:agent /workspace USER agent WORKDIR /workspace