Devices can chat with AI agents (Claude, Codex, Gemini CLI, GitHub Copilot, goose, OpenCode, or any ACP agent you add) that run on this server, next to your clusters. The agent reads a cluster through Kubyl's read-only tools (cluster info, list, get and describe objects, events, logs, top, PromQL, alerts, can-i, API resources) with the calling device's own rights, so with per-device sign-ins it sees exactly what that person may see. Nothing can be written to a cluster by an agent.
The feature is the capability agents (kubyl.v1.Agents). It is listed only while the feature is on and at least one agent is installed and signed in. An app that does not know the capability sees no difference.
Turn it on
It is off by default. Set server.agents: true in settings.json, --agents or KUBYL_SERVER_AGENTS=true (Helm chart: agents.enabled: true) and restart. Off means no capability, Agents.Status answers STATE_DISABLED, every other call answers FAILED_PRECONDITION with a message saying so, and no process is ever started.
Data flow
What the agent reads (logs, object YAML with Secret values masked, events, metrics, alerts) and what the person types goes to the agent's provider: Anthropic, OpenAI, Google, GitHub, the model provider you configured for goose or OpenCode, or the provider of your custom agent. Secret data, tokens and private keys are masked by Kubyl's tools before the agent gets them, and everything the agent says passes the same masking on its way to the device. But anything a log line or a ConfigMap contains is read as it is.
The app shows a disclosure before the first thread and asks for consent. The default text names the provider of each ready agent; replace it with server.agents_disclosure (chart: agents.disclosure).
Installing agents
The server never installs anything at run time. It looks for these programs in its PATH and starts them. The container image ships all of them; agents stay off until you turn the switch on.
| Agent | Program |
|---|---|
| Claude | claude-agent-acp |
| Codex | codex-acp |
| Gemini CLI | gemini --acp |
| GitHub Copilot | copilot --acp |
| goose | goose acp |
| OpenCode | opencode acp |
Elsewhere, run npm install --global --prefix /opt/agents <the packages> and put /opt/agents/bin into the server's PATH; goose is a native binary from its releases. A missing agent shows as *not installed* and nothing else is affected. The server looks for programs again every few minutes and whenever a thread starts, so installing one needs no restart. A custom agent goes into settings.json (a built-in id replaces that agent's command):
{"agent": {"custom": [{"id": "mine", "name": "Mine", "command": "mine-acp", "args": ["--acp"],
"env": {"MINE_TOKEN": "..."}}]}}Signing in (the server owner's job)
Open the Agents page of the web UI (same session, admin token and CSRF rules as the other pages). It lists every agent with its state (ready, needs sign-in, broken, not installed), the provider, and:
- API keys.
ANTHROPIC_API_KEY(Claude),OPENAI_API_KEYorCODEX_API_KEY(Codex),GEMINI_API_KEYorGOOGLE_API_KEY(Gemini),COPILOT_GITHUB_TOKEN(Copilot). goose and OpenCode are provider-agnostic: they take whichever ofANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY,GOOGLE_API_KEY,OPENROUTER_API_KEY,MISTRAL_API_KEYandGROQ_API_KEYyou give them. A key saved on the page is stored encrypted in the credential store, handed only to that agent's process, never shown again and never logged; Delete removes it. Only these names are accepted. - Keys from the environment. Alternatively give the server the variable in its environment (chart:
agents.existingSecret, a Secret with those keys). The server moves it out of its environment at startup and keeps it in memory for its agent, sohelmand other children never see it. A key in the server's environment reaches every agent that lists the variable (goose and OpenCode included); keys saved on the page belong to one agent. A key set on the page wins over the environment's. - Sign in. For agents that offer an ACP sign-in method there is a button per method (Codex offers a ChatGPT device code sign-in). The page shows what the agent printed, usually the verification link and code, so you can open the link on your own machine. Methods that need a browser *on the server's machine* cannot be completed headless. Claude has no sign-in method of its own over ACP: give it a key, or log in once in the agent's own home (below).
- goose needs its provider and model, which you choose. Add it to
settings.jsonwithGOOSE_PROVIDERandGOOSE_MODELin itsenv, and give it that provider's key. Without them it shows *broken*. OpenCode picks a model of the first provider that has a key; without any key it shows *needs sign-in*. Its hosted free provider is switched off on purpose, so nothing leaves without a key you gave. Sign-in with a subscription (goose configure,opencode auth login) is not offered: use keys. - Test. Starts a throwaway thread and asks one word. It sends one short prompt to the provider.
The console shows the state in its header, for example agents: claude ready, codex needs sign-in. A key the provider refuses shows up at the first prompt; the agent then turns to *needs sign-in* and the capability goes away until you fix it. The server never makes a model call on its own: only devices' prompts and the Test button call a provider.
Limits
Set them in settings.json under server.agents_limits (chart: agents.limits). Values outside the range in brackets are pulled back into it. Each running agent is a Node.js process or native binary of about 300 to 500 MiB at rest, more during a turn; the chart's agents.resources has modest defaults (512Mi requested, 2Gi limit) and the volume holds session transcripts, so size it up for many users.
| Setting | Default | Meaning |
|---|---|---|
max_threads_per_device | 3 (1 to 20) | Threads one device can have. |
max_running_turns | 4 (1 to 64) | Turns running at once on the whole server. |
idle_minutes | 30 (1 to 1440) | Idle threads are closed after this long. |
max_prompt_bytes | 8192 (256 to 262144) | Longer prompts are INVALID_ARGUMENT. |
max_tool_output_kib | 64 (8 to 1024) | What one Kubyl tool returns to the agent. The device sees the first 4 KiB of a tool's output. |
A thread belongs to the device that made it (others get NOT_FOUND); revoking a device closes its threads. Threads and transcripts are not persisted: a restart ends every conversation, and the thread folders are removed at start. Exceeding a limit is RESOURCE_EXHAUSTED.
Security notes: what the agent process can do
The agent runs with the server's uid. The server therefore:
- starts it with an allow-listed environment that the core sets up itself:
PATH, its ownHOME(<config dir>/agents/home/<agent>),TMPDIR, an emptyKUBECONFIG, locale, proxy and CA variables, and that agent's own API key. NoKUBYL_*,HELM_*,KUBERNETES_*, cloud, license or token variables, and no other agent's key.kubectlaccess is forced to none; - gives each thread a scratch folder below
<config dir>/agents/threads, removed with the thread (never/configitself); - offers no terminal and no file access: agents do not route shell and file work through Kubyl, and a request for one anyway is refused. Permission prompts for the agent's own tools are refused at once, and the thread gets one notice, “this server only allows Kubyl's cluster tools”. Only questions that make sense on a phone (forms, links) reach the device. The agents' own built-in tools are also switched off as far as each one's configuration allows;
- makes the server process non-dumpable (Linux), so an agent cannot read the server's memory or
/proc/<pid>/environ.
Mitigations that work today:
- Give the server
KUBYL_SERVER_KEY(a Secret) and deletecredentials.key, so the encrypted stores can't be opened from the config directory. - Run the image with a read-only root file system and the PVC as the only writable place.
- Set
automountServiceAccountToken: falseand keep secrets out of the environment. - Use kubeconfigs with short-lived tokens.
- Use per-device sign-ins, so the tools run with each person's rights.
- Apply an egress policy that allows only the providers' API hosts.
Agent state (conversation logs of the adapters, an OAuth login you did in the agent's home) lives in <config dir>/agents/home/<agent>. To sign Claude in with a subscription instead of a key, run claude once with HOME and CLAUDE_CONFIG_DIR set to that folder; the server notices .credentials.json there.