Architecture
Ingress paths converge on the agent pod: user chat, scheduled cron, and the operator itself. Inside the pod, one Hermes gateway hosts co-located profiles: the Planning Agent (the default profile — the conversational front door and the only chat ingress), the Platform Agent (the platform profile — the privileged specialist that does the infrastructure work), and read-only per-cluster Cluster Agents (cluster-* profiles the Platform Agent scaffolds, one per managed GKE cluster). They coordinate through a shared kanban board: the Planning Agent files a card, the gateway’s dispatcher spawns the assigned specialist as a worker, and the completion is relayed back into the originating chat thread.
Component map
Section titled “Component map”flowchart TB subgraph Ingress GC[Google Chat] SL[Slack] CR[cron jobs.json<br/>gateway ticker] end
subgraph Cluster["Kubernetes cluster"] OP[k8s-operator<br/>controller manager] CRD[PlatformAgent CR] subgraph Pod["Agent pod (k8s-deployment)"] CA["Planning Agent<br/>(default profile)"] PA["Platform Agent<br/>(platform profile)"] CL["Cluster Agents<br/>(cluster-* profiles)"] end SEC[Secret<br/>API keys, tokens] LM[LiteLLM / vLLM] MT[Minty<br/>token broker] KMS[GCP KMS keyring] end
subgraph External LLM[Gemini / Anthropic / OpenAI / Gemma] MCPGKE[GKE MCP server<br/>container.googleapis.com/mcp] GH[GitHub GitOps repo] OTEL[GKE Managed<br/>OTel + Prometheus] end
GC -->|Pub/Sub subscription| CA SL -->|Socket Mode / bot token| CA CR -->|scheduled tick| CA
CA -->|kanban card| PA PA -->|kanban card| CL
OP -->|reconciles| CRD CRD -->|owns| Pod SEC --> Pod
CA -->|Completions API| LM PA -->|Completions API| LM LM -->|hosted| LLM LM -.->|local GPU| LLM
PA -->|MCP via credential proxy| MCPGKE CL -->|MCP via credential proxy| MCPGKE MCPGKE -->|read-only tools| K8S[Kubernetes / GKE API]
PA -->|submit-suggestion| MT MT -->|sign JWT| KMS MT -->|installation token| GH
Pod -.->|OTel spans| OTEL CA -.->|reply / relayed progress| GC CA -.->|reply / relayed progress| SLThe request flows
Section titled “The request flows”1. Chat request → reply
Section titled “1. Chat request → reply”- A user posts in a Google Chat space (or DMs the Slack app).
- Google Chat publishes the event to a Pub/Sub topic; Slack routes via Socket Mode. The install’s
chat-pubsubmodule sets up the topic and subscription. - The Planning Agent (the pod’s
defaultHermes profile, imagenousresearch/hermes-agent) consumes the event through Hermes’ bundled Google Chat adapter, patched for credential-free operation bygoogle_chat_relay_patch.py(Slack usesslack_relay_patch.pyin Socket Mode). - The Planning Agent holds no infrastructure tools. It discovers the available specialists via its
routerMCP tool (list_agents, descriptions from each agent’sCAPABILITIES.md) and files a kanban card with the full request context (kanban_create); the originating chat session is auto-subscribed to the card. - The gateway’s kanban dispatcher spawns the Platform Agent (
hermes -p platform chat -q "work kanban task <id>"), which runs the tool-calling loop: system prompt (SOUL.md) + card body + available tools. Cluster reads go through thegkeMCP server; mutations that touch infra route through thesubmit-suggestionskill (see flow 3) rather than directkubectl. Work scoped to one cluster’s live runtime is delegated onward as a nested kanban card to that cluster’s read-only Cluster Agent (SOUL.md §6). - The worker’s
kanban_completeposts its fullresultback into the same Chat thread (its one-linesummaryonly when theresultadds nothing to it), plus one message for each staged sub-card completion along the way. The gateway delivers those directly rather than through the Planning Agent, so the answer arrives verbatim. Console links are built from the shared templates inagents/platform/docs/gcp-console-links.md(inlined inSOUL.md §5).
2. Cron tick → autonomous run
Section titled “2. Cron tick → autonomous run”- The Planning Agent profile’s gateway — the only ticking one, since cron ticking belongs to a running gateway — evaluates its own
jobs.jsonon schedule. Itsprofile-cron-tickjob runshermes cron tickagainst every named profile with work due, once a minute, which is what makes the Platform Agent’s own roster live (see What fires the schedule). - The governance watchdogs live on that roster,
agents/platform/cron/jobs.json. A due one runs in its own process with the Platform Agent’s persona, toolsets,skillsandmax_turns— no kanban card involved. The prompt is usually “readgovernance/<sop>.mdin your profile home and execute”; the path is relative to the profile home, whereprofile_scaffold.pyoverlays the baked/opt/platform-template/governance/directory. - The agent reads the SOP, executes the diagnostic queries, produces findings.
- The nine fleet audits hand their findings to the
fleet-auditskill, which renders and maintains one GitHub issue per audit stream — the ledger — and promotes findings with a mergeable manifest into narrow per-finding remediation PRs linked back to it. A run with no findings closes the ledger as completed and returns[SILENT]— unless it could not read the whole fleet, in which case the ledger stays open and the gaps are reported (on a scheduled run, only when they are new or changed), or it resolved findings on the way, in which case it says what closed.
The Planning Agent profile’s own job file — agents/chat/defaults/cron/jobs.json — holds nothing but no_agent script jobs: plain subprocesses, no LLM turn, since that profile’s toolsets are stripped to mcp-router, kanban and memory. Five ship: profile-cron-tick above; the hourly cluster-agent-reconcile sweep that keeps Cluster Agent profiles aligned with the live GKE fleet; oobe, which runs first-run onboarding: it delivers the inventory report, runs the inventory scan, and runs four of the fleet audits once, one after another, when the scan settles; and the old bootstrap-inventory-scan and bootstrap-inventory-delivery entries, which ship disabled.
3. Remediation → GitHub PR
Section titled “3. Remediation → GitHub PR”- The agent invokes
submit-suggestionwith a proposed diff (usually a YAML patch generated from an SOP). submit-suggestionwrites the file(s) and commits them to a topic branch, then asks the credential broker to publish it. Nothing in the agent’s container holds a token.- The broker calls Minty, which signs a JWT with a KMS-held private key and exchanges it for a short-lived GitHub App token.
- The broker pushes the branch and opens a PR against the configured GitOps repo;
submit-suggestiongets back the PR’s URL. - The agent posts a Chat message with the PR link.
Deployment topology
Section titled “Deployment topology”Everything except the LLM (and, for hosted providers, the LLM API endpoint) runs inside your Kubernetes cluster:
- Namespace:
kubeagents-system. - Node pools: Autopilot, the shape a fresh install creates, has none to size — Google schedules the operator, Platform Agent, LiteLLM, and Minty, and the sandbox costs no extra nodes. On Standard the default node pool runs everything except the Platform Agent, which runs on the gVisor pool below;
--enable-gvisor=falseputs it back with the others. If you use vLLM, provision a GPU node pool separately. - gVisor node pool for sandboxed skill execution, Standard only (the
gke-clustermodule’senable_gvisor_node_pool). The sandbox is on unless the installer’s--enable-gvisor=falseturns it off, so a Standard install gets this pool; Autopilot ships thegvisorRuntimeClass and needs no pool. - Workload Identity bindings are pre-provisioned by the
kube-agents-iammodule — the Platform Agent gets a configurable permission set (read-onlyorcustom). See Security & IAM.
Capacity and isolation
Section titled “Capacity and isolation”The hub footprint (operator, Platform Agent, LiteLLM, Minty) is small and roughly fixed — it doesn’t grow with the number of clusters the agent manages, since those are read remotely through the GKE MCP server. What actually drives node capacity:
- vLLM. Local inference needs a GPU node pool; hosted LiteLLM does not.
- gVisor. Sandboxed skill execution puts the agent pod under the
gvisorRuntimeClass, which is the default (--enable-gvisor=falseturns it off). On Autopilot, the default cluster shape, it costs no extra capacity. On Standard it means agvisor-poolto run the pod on: onee2-standard-4per zone, so three nodes on a regional cluster, with no autoscaling.
For production, prefer a dedicated cluster for the harness. The agent GSA can still be given broad GKE permissions through the custom permission set, so a hard cluster boundary limits blast radius if the execution sandbox is ever compromised. A shared cluster (with the kubeagents-system namespace for logical isolation) is fine for development.
Failure modes worth knowing
Section titled “Failure modes worth knowing”- LLM outage. LiteLLM proxies retries and can fall back to a secondary provider if configured.
SOUL.md §4retries via the recovery ladder; the agent will report the failure in Chat rather than hang. - Silent kanban workers. A delegated card only reaches the chat thread on completion, block, or a failure. The Platform Agent is instructed (
SOUL.md §0) to report progress on long jobs withkanban_heartbeatnotes, and any sub-card it creates inherits the chat subscription, so progress stays visible. A worker must end withkanban_completeorkanban_block. One that ends its turn with a plain-text answer is nudged up to twice to close the card; if it still does not, the card is charged atimed_outfailure whose error opensmodel replied in text and made no terminal kanban calland reports how many nudges it got. It shows in the thread as a timed-out line, but no clock expired, and the answer was not delivered: the card was not closed and nothing was saved as its result. The dispatcher runs the card again. A second failure in a row blocks the card;hermes kanban show <id>lists each attempt’s error, and unblocking the card runs it again. Exits that exhaust a turn or retry limit are charged the same way;spec.harness.tuningcovers those. - Cron overlap. A long-running fleet audit can still be going when the next job fires — the original seven audits start thirty minutes apart, the two GCP audits land between them with the closest pair five minutes apart, and
github-repo-watcherticks every 10 minutes regardless. Each dispatcher tick is its ownhermes cron tickprocess, and each job holds a per-job lock while it runs, so a job never overlaps itself but different jobs do run concurrently; what a slow audit costs the others is at most a one-minute delay of the dispatcher tick behind it. The watcher is a script rather than a model run, so it waits on nothing. Autonomous watchdogs has the detail. - Minty token exhaustion. GitHub App installation tokens are 1-hour scoped. Minty caches and re-mints as needed; if the token refresher fails,
submit-suggestionwill surface it and route through the recovery ladder.github-repo-watcherhas no ladder to route through — it is a script, not a model run — so a broker that refuses surfaces in the room as a reason code:FORGE_UNAUTHENTICATEDwhen the forge would not accept this install’s credential,BROKER_UNREACHABLEorSANDBOX_UNREACHABLEwhen the call never got that far,FORGE_NOT_FOUNDorFORGE_RATE_LIMITEDfor the forge’s own answers,BROKER_ROUTE_UNSUPPORTEDfor a broker that answered and does not serve these routes, andREPO_UNREACHABLEwhen every managed repository refused and their codes disagreed, so the per-repository codes are the ones to read. A refusal carrying no code at all falls toBROKER_UNREACHABLE. Which of those it is distinguishes a credential the forge rejected from a transport that is down — a distinction the operator needs, because only the first is about the token. It does not distinguish a fresh token the forge rejected from a refresh that never ran: a failed mint is logged and not raised, the verb goes ahead on whatever the broker already held, and the forge’s 401 is the same either way. The proxy container’s log below is what separates them. Why the forge rejected it is not carried with the code: it is rendered into a chat room, so it stays detail-free. The refusal itself is logged by theenvoy-credential-proxycontainer, which runs the refresh helper and records its stderr with token-shaped substrings redacted; read that, or Minty’s own logs. - Pub/Sub subscription drift. If the GChat Pub/Sub subscription is deleted out of band, no chat messages reach the Planning Agent and no error surfaces automatically. Cron-driven runs still work, and the
kube-agents-observabilityskill helps triage.
Where to go next
Section titled “Where to go next”- Platform Agent — the persona, safety rails, and MCP wiring.
- ChatOps — Google Chat and Slack ingress details.
- Declarative workflow — how PR-based remediation is enforced.
docs/designs/agent-communication.md— the design of record for inter-agent coordination (kanban delegation today; a file-based handover channel as the proposed next step).