diff --git a/.github/workflows/reusable-e2e.yaml b/.github/workflows/reusable-e2e.yaml index 2a949169c..1b10029cc 100644 --- a/.github/workflows/reusable-e2e.yaml +++ b/.github/workflows/reusable-e2e.yaml @@ -119,6 +119,7 @@ jobs: kind load docker-image ghcr.io/kelos-dev/gemini:e2e kind load docker-image ghcr.io/kelos-dev/opencode:e2e kind load docker-image ghcr.io/kelos-dev/cursor:e2e + kind load docker-image ghcr.io/kelos-dev/antigravity:e2e kind load docker-image ghcr.io/kelos-dev/kelos-slack-server:e2e - name: Build CLI diff --git a/Makefile b/Makefile index 4ac50259a..4c202abb5 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Image configuration REGISTRY ?= ghcr.io/kelos-dev VERSION ?= latest -IMAGE_DIRS ?= cmd/kelos-controller cmd/kelos-spawner cmd/ghproxy cmd/kelos-webhook-server claude-code codex gemini opencode cursor cmd/kelos-slack-server +IMAGE_DIRS ?= cmd/kelos-controller cmd/kelos-spawner cmd/ghproxy cmd/kelos-webhook-server claude-code codex gemini opencode cursor antigravity cmd/kelos-slack-server LOCAL_ARCH ?= $(shell go env GOARCH) # Version injection for the kelos CLI – only set ldflags when an explicit diff --git a/README.md b/README.md index feb66053f..bed4bd606 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Kelos is a Kubernetes-native framework for AI coding agents. It does two things: 1. **Defines the agent and the environment it runs in as one unit** — the prompt, model, instructions, plugins, MCP servers, git workspace, credentials, and Pod resources all live together as Kubernetes resources you can version-control. 2. **Defines how agents integrate with your workflows** — trigger runs from GitHub issues, PRs, webhooks, Linear, Jira, schedules, or any HTTP source, and chain agents into pipelines. -Supports **Claude Code**, **OpenAI Codex**, **Google Gemini**, **OpenCode**, **Cursor**, and [custom agent images](docs/agent-image-interface.md). +Supports **Claude Code**, **OpenAI Codex**, **Google Gemini**, **OpenCode**, **Cursor**, **Google Antigravity**, and [custom agent images](docs/agent-image-interface.md). ## How It Works @@ -53,7 +53,7 @@ AI coding agents are evolving from interactive CLI tools into autonomous backgro - **Workflow as YAML** — Define your development workflow declaratively: what triggers agents, what they do, and how they hand off. Version-control it, review it in PRs, and GitOps it like any other infrastructure. - **Orchestration, not just execution** — Don't just run an agent; manage its entire lifecycle. Chain tasks with `dependsOn` and pass results (branch names, PR URLs, token usage) between pipeline stages. Use `TaskSpawner` to build event-driven workers that react to GitHub issues, PRs, or schedules. - **Host-isolated autonomy** — Each task runs in an isolated, ephemeral Pod with a freshly cloned git workspace. Agents have no access to your host machine — use [scoped tokens and branch protection](#security-considerations) to control repository access. -- **Standardized interface** — Plug in any agent (Claude, Codex, Gemini, OpenCode, Cursor, or your own) using a simple [container interface](docs/agent-image-interface.md). Kelos handles credential injection, workspace management, and Kubernetes plumbing. +- **Standardized interface** — Plug in any agent (Claude, Codex, Gemini, OpenCode, Cursor, Antigravity, or your own) using a simple [container interface](docs/agent-image-interface.md). Kelos handles credential injection, workspace management, and Kubernetes plumbing. - **Scalable parallelism** — Fan out agents across multiple repositories. Kubernetes handles scheduling, resource management, and queueing — scale is limited by your cluster capacity and API provider quotas. - **Observable & CI-native** — Every agent run is a first-class Kubernetes resource with deterministic outputs (branch names, PR URLs, commit SHAs, token usage) captured into status. Monitor via `kubectl`, manage via the `kelos` CLI or declarative YAML (GitOps-ready), and integrate with ArgoCD or GitHub Actions. @@ -581,7 +581,7 @@ kelos resume taskspawner my-spawner
What agents does Kelos support? -Kelos supports **Claude Code**, **OpenAI Codex**, **Google Gemini**, **OpenCode**, and **Cursor** out of the box. You can also bring your own agent image using the [container interface](docs/agent-image-interface.md). +Kelos supports **Claude Code**, **OpenAI Codex**, **Google Gemini**, **OpenCode**, **Cursor**, and **Google Antigravity** out of the box. You can also bring your own agent image using the [container interface](docs/agent-image-interface.md).
diff --git a/antigravity/Dockerfile b/antigravity/Dockerfile new file mode 100644 index 000000000..d8782cb77 --- /dev/null +++ b/antigravity/Dockerfile @@ -0,0 +1,50 @@ +FROM ubuntu:24.04 + +ARG GO_VERSION=1.25.0 + +RUN apt-get update && apt-get install -y \ + make \ + curl \ + ca-certificates \ + git \ + && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs \ + && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +RUN ARCH=$(dpkg --print-architecture) \ + && TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz" \ + && curl -fsSL -o "/tmp/${TARBALL}" "https://dl.google.com/go/${TARBALL}" \ + && curl -fsSL -o "/tmp/${TARBALL}.sha256" "https://dl.google.com/go/${TARBALL}.sha256" \ + && echo "$(cat /tmp/${TARBALL}.sha256) /tmp/${TARBALL}" | sha256sum -c - \ + && tar -C /usr/local -xzf "/tmp/${TARBALL}" \ + && rm "/tmp/${TARBALL}" "/tmp/${TARBALL}.sha256" + +ENV PATH="/usr/local/go/bin:${PATH}" + +COPY antigravity/kelos_entrypoint.sh /kelos_entrypoint.sh +RUN chmod +x /kelos_entrypoint.sh + +ARG TARGETARCH +COPY bin/kelos-capture-linux-${TARGETARCH} /kelos/kelos-capture + +RUN useradd -u 61100 -m -s /bin/bash agent +RUN mkdir -p /home/agent/.config/agy /home/agent/.local/bin && chown -R agent:agent /home/agent + +USER agent + +RUN curl -fsSL https://antigravity.google/cli/install.sh -o /tmp/install-agy.sh \ + && bash /tmp/install-agy.sh \ + && rm /tmp/install-agy.sh + +ENV GOPATH="/home/agent/go" +ENV PATH="/home/agent/.local/bin:${GOPATH}/bin:${PATH}" + +WORKDIR /workspace + +ENTRYPOINT ["agy"] diff --git a/antigravity/kelos_entrypoint.sh b/antigravity/kelos_entrypoint.sh new file mode 100755 index 000000000..27d519c89 --- /dev/null +++ b/antigravity/kelos_entrypoint.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# kelos_entrypoint.sh — Kelos agent image interface implementation for +# Google Antigravity CLI (agy). +# +# Interface contract: +# - First argument ($1): the task prompt +# - KELOS_MODEL env var: model name (optional) +# - KELOS_AGENTS_MD env var: user-level instructions (optional) +# - UID 61100: shared between git-clone init container and agent +# - Working directory: /workspace/repo when a workspace is configured +# +# Credentials: the controller does not inject built-in credential env vars +# for this agent type (Task.spec.credentials.type must be "none"). Operators +# who need authenticated runs supply credentials via Task.spec.podOverrides.env +# using whatever variable the agy binary expects. + +set -uo pipefail + +PROMPT="${1:?Prompt argument is required}" + +ARGS=( + "--print" + "--output-format" "stream-json" +) + +if [ -n "${KELOS_MODEL:-}" ]; then + ARGS+=("--model" "$KELOS_MODEL") +fi + +ARGS+=("$PROMPT") + +# Write user-level instructions (global scope read by Antigravity CLI). +if [ -n "${KELOS_AGENTS_MD:-}" ]; then + mkdir -p ~/.config/agy + printf '%s' "$KELOS_AGENTS_MD" >~/.config/agy/AGENTS.md +fi + +# Run pre-agent setup command if configured. KELOS_SETUP_COMMAND is the +# JSON-encoded exec-form array from Workspace.spec.setupCommand. A non-zero +# exit aborts the task before the agent starts. +if [ -n "${KELOS_SETUP_COMMAND:-}" ]; then + printf '\n---KELOS_SETUP_COMMAND_START---\n' >&2 + node -e ' +const { spawnSync } = require("child_process"); +const cmd = JSON.parse(process.env.KELOS_SETUP_COMMAND); +if (!Array.isArray(cmd) || cmd.length === 0 || cmd.some(a => typeof a !== "string")) { + console.error("KELOS_SETUP_COMMAND must be a non-empty JSON array of strings"); + process.exit(2); +} +const r = spawnSync(cmd[0], cmd.slice(1), { stdio: "inherit" }); +if (r.error) { console.error(r.error.message); process.exit(127); } +process.exit(r.status ?? 1); +' + SETUP_EXIT_CODE=$? + if [ "$SETUP_EXIT_CODE" -ne 0 ]; then + printf '\n---KELOS_SETUP_COMMAND_FAILED--- exit=%s\n' "$SETUP_EXIT_CODE" >&2 + exit "$SETUP_EXIT_CODE" + fi + printf '\n---KELOS_SETUP_COMMAND_DONE---\n' >&2 +fi + +agy "${ARGS[@]}" | tee /tmp/agent-output.jsonl +AGENT_EXIT_CODE=${PIPESTATUS[0]} + +/kelos/kelos-capture + +exit $AGENT_EXIT_CODE diff --git a/api/v1alpha1/task_types.go b/api/v1alpha1/task_types.go index 75c2b4be3..9295c1558 100644 --- a/api/v1alpha1/task_types.go +++ b/api/v1alpha1/task_types.go @@ -139,7 +139,7 @@ type PodOverrides struct { type TaskSpec struct { // Type specifies the agent type (e.g., claude-code). // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=claude-code;codex;gemini;opencode;cursor + // +kubebuilder:validation:Enum=claude-code;codex;gemini;opencode;cursor;antigravity Type string `json:"type"` // Prompt is the task prompt to send to the agent. diff --git a/api/v1alpha1/taskspawner_types.go b/api/v1alpha1/taskspawner_types.go index 626fa1d17..c61c3823e 100644 --- a/api/v1alpha1/taskspawner_types.go +++ b/api/v1alpha1/taskspawner_types.go @@ -813,7 +813,7 @@ type TaskTemplateMetadata struct { type TaskTemplate struct { // Type specifies the agent type (e.g., claude-code). // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=claude-code;codex;gemini;opencode;cursor + // +kubebuilder:validation:Enum=claude-code;codex;gemini;opencode;cursor;antigravity Type string `json:"type"` // Credentials specifies how to authenticate with the agent. diff --git a/cmd/kelos-controller/main.go b/cmd/kelos-controller/main.go index ed04bcc4d..4dc754bb5 100644 --- a/cmd/kelos-controller/main.go +++ b/cmd/kelos-controller/main.go @@ -49,6 +49,8 @@ func main() { var openCodeImagePullPolicy string var cursorImage string var cursorImagePullPolicy string + var antigravityImage string + var antigravityImagePullPolicy string var spawnerImage string var spawnerImagePullPolicy string var spawnerResourceRequests string @@ -77,6 +79,8 @@ func main() { flag.StringVar(&openCodeImagePullPolicy, "opencode-image-pull-policy", "", "The image pull policy for OpenCode agent containers (e.g., Always, Never, IfNotPresent).") flag.StringVar(&cursorImage, "cursor-image", controller.CursorImage, "The image to use for Cursor CLI agent containers.") flag.StringVar(&cursorImagePullPolicy, "cursor-image-pull-policy", "", "The image pull policy for Cursor CLI agent containers (e.g., Always, Never, IfNotPresent).") + flag.StringVar(&antigravityImage, "antigravity-image", controller.AntigravityImage, "The image to use for Antigravity CLI agent containers.") + flag.StringVar(&antigravityImagePullPolicy, "antigravity-image-pull-policy", "", "The image pull policy for Antigravity CLI agent containers (e.g., Always, Never, IfNotPresent).") flag.StringVar(&spawnerImage, "spawner-image", controller.DefaultSpawnerImage, "The image to use for spawner Deployments.") flag.StringVar(&spawnerImagePullPolicy, "spawner-image-pull-policy", "", "The image pull policy for spawner Deployments (e.g., Always, Never, IfNotPresent).") flag.StringVar(&spawnerResourceRequests, "spawner-resource-requests", "", "Resource requests for spawner containers as comma-separated name=value pairs (e.g., cpu=250m,memory=512Mi).") @@ -195,6 +199,8 @@ func main() { jobBuilder.OpenCodeImagePullPolicy = corev1.PullPolicy(openCodeImagePullPolicy) jobBuilder.CursorImage = cursorImage jobBuilder.CursorImagePullPolicy = corev1.PullPolicy(cursorImagePullPolicy) + jobBuilder.AntigravityImage = antigravityImage + jobBuilder.AntigravityImagePullPolicy = corev1.PullPolicy(antigravityImagePullPolicy) if err = (&controller.TaskReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/docs/agent-image-interface.md b/docs/agent-image-interface.md index f28502a67..2c28ba2a7 100644 --- a/docs/agent-image-interface.md +++ b/docs/agent-image-interface.md @@ -40,7 +40,7 @@ Kelos sets the following reserved environment variables on agent containers: | `GH_TOKEN` | GitHub token for `gh` CLI (github.com) | When workspace has a `secretRef` and repo is on github.com | | `GH_ENTERPRISE_TOKEN` | GitHub token for `gh` CLI (GitHub Enterprise) | When workspace has a `secretRef` and repo is on a GitHub Enterprise host | | `GH_HOST` | Hostname for GitHub Enterprise | When repo is on a GitHub Enterprise host | -| `KELOS_AGENT_TYPE` | The agent type (`claude-code`, `codex`, `gemini`, `opencode`, `cursor`) | Always | +| `KELOS_AGENT_TYPE` | The agent type (`claude-code`, `codex`, `gemini`, `opencode`, `cursor`, `antigravity`) | Always | | `KELOS_BASE_BRANCH` | The base branch (workspace `ref`) for the task | When workspace has a non-empty `ref` | | `KELOS_AGENTS_MD` | User-level instructions from AgentConfig | When `agentConfigRef` is set and `agentsMD` is non-empty | | `KELOS_PLUGIN_DIR` | Path to plugin directory containing skills and agents | When `agentConfigRef` is set and `plugins` is non-empty | @@ -178,3 +178,16 @@ the agent exits non-zero. - `gemini/kelos_entrypoint.sh` — wraps the `gemini` CLI (Google Gemini). - `opencode/kelos_entrypoint.sh` — wraps the `opencode` CLI (OpenCode). - `cursor/kelos_entrypoint.sh` — wraps the `agent` CLI (Cursor). +- `antigravity/kelos_entrypoint.sh` — wraps the `agy` CLI (Google Antigravity). + +## Agent-type-specific notes + +### `antigravity` + +The Antigravity (`agy`) CLI does not currently have a documented +non-interactive API-key or OAuth-token environment variable, so Kelos +restricts `Task.spec.credentials.type` to `none` for this agent type and +the controller does not inject a built-in credential env var. Operators +who need authenticated runs should supply credentials via +`Task.spec.podOverrides.env` (or a mounted Secret) using whatever variable +the `agy` binary expects. diff --git a/docs/reference.md b/docs/reference.md index 957763ec7..394e30755 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -4,7 +4,7 @@ | Field | Description | Required | |-------|-------------|----------| -| `spec.type` | Agent type (`claude-code`, `codex`, `gemini`, `opencode`, or `cursor`) | Yes | +| `spec.type` | Agent type (`claude-code`, `codex`, `gemini`, `opencode`, `cursor`, or `antigravity`) | Yes | | `spec.prompt` | Task prompt for the agent | Yes | | `spec.credentials.type` | `api-key`, `oauth`, or `none`. Use `none` to skip built-in credential injection (e.g., for Bedrock, Vertex AI, or Azure OpenAI credentials provided via `podOverrides.env`) | Yes | | `spec.credentials.secretRef.name` | Secret name with credentials (see [secret format](#task-credential-secret-format) below; not required when `type` is `none`) | Conditional | @@ -59,6 +59,7 @@ The secret referenced by `spec.credentials.secretRef.name` must contain a single | `gemini` | `api-key` or `oauth` | `GEMINI_API_KEY` | | `opencode` | `api-key` or `oauth` | `OPENCODE_API_KEY` | | `cursor` | `api-key` or `oauth` | `CURSOR_API_KEY` | +| `antigravity` | only `none` supported | n/a — supply credentials via `spec.podOverrides.env` | Example for `claude-code` with an API key: @@ -244,7 +245,7 @@ GitHub Apps are preferred over PATs for production use because they offer fine-g | `spec.when.webhook.filters[].pattern` | Require a regex match against the extracted field value (mutually exclusive with `value`) | Conditional | | `spec.when.jira.pollInterval` | Per-source poll interval override (e.g., `"30s"`, `"5m"`); takes precedence over `spec.pollInterval` | No | | `spec.when.cron.schedule` | Cron schedule expression (e.g., `"0 * * * *"`) | Yes (when using cron) | -| `spec.taskTemplate.type` | Agent type (`claude-code`, `codex`, `gemini`, `opencode`, or `cursor`) | Yes | +| `spec.taskTemplate.type` | Agent type (`claude-code`, `codex`, `gemini`, `opencode`, `cursor`, or `antigravity`) | Yes | | `spec.taskTemplate.credentials` | Credentials for the agent (same as Task) | Yes | | `spec.taskTemplate.model` | Model override for spawned Tasks. Same pass-through behavior as `Task.spec.model`: either an agent-native shorthand (e.g., `sonnet`, `opus` for Claude Code) or a versioned ID (e.g., `claude-sonnet-4-6`) is valid | No | | `spec.taskTemplate.image` | Custom agent image override (see [Agent Image Interface](agent-image-interface.md)) | No | @@ -397,7 +398,7 @@ The `token` and `githubApp` fields are mutually exclusive. If both `name` and `r | Field | Description | |-------|-------------| -| `type` | Default agent type (`claude-code`, `codex`, `gemini`, `opencode`, or `cursor`) | +| `type` | Default agent type (`claude-code`, `codex`, `gemini`, `opencode`, `cursor`, or `antigravity`) | | `model` | Default model override | | `namespace` | Default Kubernetes namespace | | `agentConfig` | Default AgentConfig resource name | diff --git a/internal/cli/run.go b/internal/cli/run.go index 1cab0a059..a53f6e9b2 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -345,7 +345,7 @@ func newRunCommand(cfg *ClientConfig) *cobra.Command { cmd.MarkFlagsMutuallyExclusive("prompt", "prompt-file") _ = cmd.RegisterFlagCompletionFunc("credential-type", cobra.FixedCompletions([]string{"api-key", "oauth", "none"}, cobra.ShellCompDirectiveNoFileComp)) - _ = cmd.RegisterFlagCompletionFunc("type", cobra.FixedCompletions([]string{"claude-code", "codex", "gemini", "opencode", "cursor"}, cobra.ShellCompDirectiveNoFileComp)) + _ = cmd.RegisterFlagCompletionFunc("type", cobra.FixedCompletions([]string{"claude-code", "codex", "gemini", "opencode", "cursor", "antigravity"}, cobra.ShellCompDirectiveNoFileComp)) return cmd } diff --git a/internal/controller/job_builder.go b/internal/controller/job_builder.go index 035aab379..f69b2f8c9 100644 --- a/internal/controller/job_builder.go +++ b/internal/controller/job_builder.go @@ -30,6 +30,9 @@ const ( // CursorImage is the default image for Cursor CLI agent. CursorImage = "ghcr.io/kelos-dev/cursor:latest" + // AntigravityImage is the default image for Google Antigravity CLI agent. + AntigravityImage = "ghcr.io/kelos-dev/antigravity:latest" + // AgentTypeClaudeCode is the agent type for Claude Code. AgentTypeClaudeCode = "claude-code" @@ -45,6 +48,9 @@ const ( // AgentTypeCursor is the agent type for Cursor CLI. AgentTypeCursor = "cursor" + // AgentTypeAntigravity is the agent type for Google Antigravity CLI. + AgentTypeAntigravity = "antigravity" + // GitCloneImage is the image used for cloning git repositories. GitCloneImage = "alpine/git:v2.47.2" @@ -91,26 +97,29 @@ var reservedEnvNames = map[string]struct{}{ // JobBuilder constructs Kubernetes Jobs for Tasks. type JobBuilder struct { - ClaudeCodeImage string - ClaudeCodeImagePullPolicy corev1.PullPolicy - CodexImage string - CodexImagePullPolicy corev1.PullPolicy - GeminiImage string - GeminiImagePullPolicy corev1.PullPolicy - OpenCodeImage string - OpenCodeImagePullPolicy corev1.PullPolicy - CursorImage string - CursorImagePullPolicy corev1.PullPolicy + ClaudeCodeImage string + ClaudeCodeImagePullPolicy corev1.PullPolicy + CodexImage string + CodexImagePullPolicy corev1.PullPolicy + GeminiImage string + GeminiImagePullPolicy corev1.PullPolicy + OpenCodeImage string + OpenCodeImagePullPolicy corev1.PullPolicy + CursorImage string + CursorImagePullPolicy corev1.PullPolicy + AntigravityImage string + AntigravityImagePullPolicy corev1.PullPolicy } // NewJobBuilder creates a new JobBuilder. func NewJobBuilder() *JobBuilder { return &JobBuilder{ - ClaudeCodeImage: ClaudeCodeImage, - CodexImage: CodexImage, - GeminiImage: GeminiImage, - OpenCodeImage: OpenCodeImage, - CursorImage: CursorImage, + ClaudeCodeImage: ClaudeCodeImage, + CodexImage: CodexImage, + GeminiImage: GeminiImage, + OpenCodeImage: OpenCodeImage, + CursorImage: CursorImage, + AntigravityImage: AntigravityImage, } } @@ -128,6 +137,11 @@ func (b *JobBuilder) Build(task *kelosv1alpha1.Task, workspace *kelosv1alpha1.Wo return b.buildAgentJob(task, workspace, agentConfig, b.OpenCodeImage, b.OpenCodeImagePullPolicy, prompt) case AgentTypeCursor: return b.buildAgentJob(task, workspace, agentConfig, b.CursorImage, b.CursorImagePullPolicy, prompt) + case AgentTypeAntigravity: + if task.Spec.Credentials.Type != kelosv1alpha1.CredentialTypeNone { + return nil, fmt.Errorf("agent type %q requires credentials.type %q (api-key and oauth are not yet supported)", AgentTypeAntigravity, kelosv1alpha1.CredentialTypeNone) + } + return b.buildAgentJob(task, workspace, agentConfig, b.AntigravityImage, b.AntigravityImagePullPolicy, prompt) default: return nil, fmt.Errorf("unsupported agent type: %s", task.Spec.Type) } diff --git a/internal/controller/job_builder_test.go b/internal/controller/job_builder_test.go index 0a03c6102..0a6df7521 100644 --- a/internal/controller/job_builder_test.go +++ b/internal/controller/job_builder_test.go @@ -1712,6 +1712,163 @@ func TestBuildCursorJob_OAuthCredentials(t *testing.T) { } } +func TestBuildAntigravityJob_DefaultImage(t *testing.T) { + builder := NewJobBuilder() + task := &kelosv1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-antigravity", + Namespace: "default", + }, + Spec: kelosv1alpha1.TaskSpec{ + Type: AgentTypeAntigravity, + Prompt: "Fix the bug", + Credentials: kelosv1alpha1.Credentials{ + Type: kelosv1alpha1.CredentialTypeNone, + }, + Model: "gemini-2.5-pro", + }, + } + + job, err := builder.Build(task, nil, nil, task.Spec.Prompt) + if err != nil { + t.Fatalf("Build() returned error: %v", err) + } + + container := job.Spec.Template.Spec.Containers[0] + + if container.Image != AntigravityImage { + t.Errorf("Expected image %q, got %q", AntigravityImage, container.Image) + } + + if container.Name != AgentTypeAntigravity { + t.Errorf("Expected container name %q, got %q", AgentTypeAntigravity, container.Name) + } + + if len(container.Command) != 1 || container.Command[0] != "/kelos_entrypoint.sh" { + t.Errorf("Expected command [/kelos_entrypoint.sh], got %v", container.Command) + } + + if len(container.Args) != 1 || container.Args[0] != "Fix the bug" { + t.Errorf("Expected args [Fix the bug], got %v", container.Args) + } + + foundKelosModel := false + foundAgentType := false + for _, env := range container.Env { + if env.Name == "KELOS_MODEL" { + foundKelosModel = true + if env.Value != "gemini-2.5-pro" { + t.Errorf("KELOS_MODEL value: expected %q, got %q", "gemini-2.5-pro", env.Value) + } + } + if env.Name == "KELOS_AGENT_TYPE" { + foundAgentType = true + if env.Value != AgentTypeAntigravity { + t.Errorf("KELOS_AGENT_TYPE value: expected %q, got %q", AgentTypeAntigravity, env.Value) + } + } + if env.Name == "ANTHROPIC_API_KEY" || + env.Name == "CODEX_API_KEY" || + env.Name == "GEMINI_API_KEY" || + env.Name == "OPENCODE_API_KEY" || + env.Name == "CURSOR_API_KEY" || + env.Name == "CLAUDE_CODE_OAUTH_TOKEN" { + t.Errorf("%s should not be set for antigravity agent type (credentials.type: none)", env.Name) + } + } + if !foundKelosModel { + t.Error("Expected KELOS_MODEL env var to be set") + } + if !foundAgentType { + t.Error("Expected KELOS_AGENT_TYPE env var to be set") + } +} + +func TestBuildAntigravityJob_CustomImage(t *testing.T) { + builder := NewJobBuilder() + task := &kelosv1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-antigravity-custom", + Namespace: "default", + }, + Spec: kelosv1alpha1.TaskSpec{ + Type: AgentTypeAntigravity, + Prompt: "Refactor the module", + Credentials: kelosv1alpha1.Credentials{ + Type: kelosv1alpha1.CredentialTypeNone, + }, + Image: "my-antigravity:v2", + }, + } + + job, err := builder.Build(task, nil, nil, task.Spec.Prompt) + if err != nil { + t.Fatalf("Build() returned error: %v", err) + } + + container := job.Spec.Template.Spec.Containers[0] + + if container.Image != "my-antigravity:v2" { + t.Errorf("Expected image %q, got %q", "my-antigravity:v2", container.Image) + } + + if len(container.Command) != 1 || container.Command[0] != "/kelos_entrypoint.sh" { + t.Errorf("Expected command [/kelos_entrypoint.sh], got %v", container.Command) + } +} + +func TestBuildAntigravityJob_RejectsAPIKey(t *testing.T) { + builder := NewJobBuilder() + task := &kelosv1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-antigravity-apikey", + Namespace: "default", + }, + Spec: kelosv1alpha1.TaskSpec{ + Type: AgentTypeAntigravity, + Prompt: "Hello", + Credentials: kelosv1alpha1.Credentials{ + Type: kelosv1alpha1.CredentialTypeAPIKey, + SecretRef: &kelosv1alpha1.SecretReference{Name: "agy-secret"}, + }, + }, + } + + _, err := builder.Build(task, nil, nil, task.Spec.Prompt) + if err == nil { + t.Fatal("Expected error when antigravity is paired with api-key credentials, got nil") + } + if !strings.Contains(err.Error(), AgentTypeAntigravity) { + t.Errorf("Error should mention agent type %q: %v", AgentTypeAntigravity, err) + } + if !strings.Contains(err.Error(), "none") { + t.Errorf("Error should mention required credential type %q: %v", "none", err) + } +} + +func TestBuildAntigravityJob_RejectsOAuth(t *testing.T) { + builder := NewJobBuilder() + task := &kelosv1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-antigravity-oauth", + Namespace: "default", + }, + Spec: kelosv1alpha1.TaskSpec{ + Type: AgentTypeAntigravity, + Prompt: "Hello", + Credentials: kelosv1alpha1.Credentials{ + Type: kelosv1alpha1.CredentialTypeOAuth, + SecretRef: &kelosv1alpha1.SecretReference{Name: "agy-oauth"}, + }, + }, + } + + _, err := builder.Build(task, nil, nil, task.Spec.Prompt) + if err == nil { + t.Fatal("Expected error when antigravity is paired with oauth credentials, got nil") + } +} + func TestBuildClaudeCodeJob_UnsupportedType(t *testing.T) { builder := NewJobBuilder() task := &kelosv1alpha1.Task{ diff --git a/internal/manifests/charts/kelos/templates/crds/task-crd.yaml b/internal/manifests/charts/kelos/templates/crds/task-crd.yaml index 372b812bf..1084ee31a 100644 --- a/internal/manifests/charts/kelos/templates/crds/task-crd.yaml +++ b/internal/manifests/charts/kelos/templates/crds/task-crd.yaml @@ -3812,6 +3812,7 @@ spec: - gemini - opencode - cursor + - antigravity type: string upstreamRepo: description: |- diff --git a/internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml b/internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml index e289d0ab6..e159b7d5e 100644 --- a/internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml +++ b/internal/manifests/charts/kelos/templates/crds/taskspawner-crd.yaml @@ -4059,6 +4059,7 @@ spec: - gemini - opencode - cursor + - antigravity type: string upstreamRepo: description: |- diff --git a/internal/manifests/install-crd.yaml b/internal/manifests/install-crd.yaml index 42ad29ee9..24fca2d06 100644 --- a/internal/manifests/install-crd.yaml +++ b/internal/manifests/install-crd.yaml @@ -4036,6 +4036,7 @@ spec: - gemini - opencode - cursor + - antigravity type: string upstreamRepo: description: |- @@ -8165,6 +8166,7 @@ spec: - gemini - opencode - cursor + - antigravity type: string upstreamRepo: description: |- diff --git a/skills/kelos/SKILL.md b/skills/kelos/SKILL.md index f8f2f985f..52d6b9c10 100644 --- a/skills/kelos/SKILL.md +++ b/skills/kelos/SKILL.md @@ -44,7 +44,7 @@ Kelos defines four custom resources: A Task runs an AI agent with a prompt. Key fields: -- `spec.type` (required): `claude-code`, `codex`, `gemini`, `opencode`, or `cursor` +- `spec.type` (required): `claude-code`, `codex`, `gemini`, `opencode`, `cursor`, or `antigravity` - `spec.prompt` (required): The task prompt - `spec.credentials` (required): `type` (`api-key` or `oauth`) and `secretRef.name` - `spec.workspaceRef.name`: Reference to a Workspace @@ -243,6 +243,7 @@ Available result keys: `branch`, `commit`, `base-branch`, `pr`, `input-tokens`, | `gemini` | `gemini` | `GEMINI_API_KEY` | | `opencode` | `opencode` | `OPENCODE_API_KEY` | | `cursor` | `agent` (Cursor) | `CURSOR_API_KEY` | +| `antigravity` | `agy` (Google Antigravity) | n/a (only `credentials.type: none` supported; supply credentials via `podOverrides.env`) | ## References