Skip to content

Platform Agent

The Platform Agent is an autonomous agent with a defined role — Platform Custodian and Agent Architect. It’s not a general-purpose Kubernetes assistant. The rules of its behavior are codified in SOUL.md, which the Hermes runtime loads as the system prompt.

It runs as the platform Hermes profile in the agent pod, scaffolded at pod startup from the agents/platform/ template by profile_scaffold.py. It does not receive chat directly: the Planning Agent (the pod’s default profile — see ChatOps) is the conversational front door and delegates work to the Platform Agent as cards on the shared kanban board. Per SOUL.md §0, the Platform Agent is invoked with work kanban task <id>, reads the request with kanban_show, does the work, and always finishes with kanban_complete or kanban_block. A completed card’s result field carries the answer and is posted verbatim into the chat thread, so that — not the one-line summary — is what the user actually receives; result is a required argument, enforced by a patch the image applies to the kanban_complete tool (deploy/docker/patches/kanban_result_required.py). Because that answer is rendered as Slack blocks, its shape is specified too: kanban_create appends a report-format stanza to every card body that does not already give format instructions (deploy/docker/patches/kanban_report_format.py). That stanza is an instruction, not a gate — shape is never a reason to refuse a completion, because a report thrown away for its formatting answers nothing at all. The delivery path measures the result and logs what it finds: a # H1 duplicating the card title the chat message already shows, or ASCII section markers Slack renders as flat text, raise a WARNING naming the edit; cosmetic defects go to INFO. A blank result, and a completion over the card’s own unfinished fan-out children (which is told to wait for them and synthesize instead), are each refused at most once per card before the card closes with whatever the worker sends (deploy/docker/patches/kanban_children_settled.py). The same patch refuses kanban_block(kind="dependency") on a card with nothing unfinished to wait on (no unfinished parent, and no fan-out child the scheduling repair turns into one), because the board would hand that card straight to a fresh worker that can’t see what the first one already did. kanban_link is refused once the child has started (running, claimed, or any run on record), because the link would hold that run behind the new parent; the worker is told to wait for a running child’s result, or that a blocked one needs an unblock rather than a link (deploy/docker/patches/kanban_link_running_guard.py). If someone else blocks the card while it runs, its worker is stopped, and a worker whose run has ended is told to stop rather than retry (deploy/docker/patches/kanban_ended_run.py). The same patch holds a card to a plan when it carries one: step <key>: planned lines, one per deliverable, in the card’s body or its comments, where <key> starts with a letter and is letters, digits and hyphens, at most 40 characters, and the status ends the line (up to three closing marks such as ., ! or an emoji are allowed, a checklist box may lead, and backticks and * around the key or status are ignored) or is followed by a separated reason (step substrate: failed - helm has no credentials). The worker moves each to done or failed with kanban_comment, and the latest line per key counts. Only comments by the card’s assignee count, so an answer the Planning Agent relays onto a needs_input card can’t rewrite the record. A step the requester drops after a block is recorded by the worker as step <key>: done - dropped at the requester's request, since an answer the Planning Agent relays doesn’t count on its own. While any declared step is not done, kanban_complete is refused on every attempt, not once, so a card whose deliverable was not delivered cannot close as done: the worker blocks it, naming what was left behind, or runs out of turns. A card with no step lines is unaffected. On a long job it reports progress as it goes with kanban_heartbeat(note=…); the notes accumulate into a single rolling ⏳ message that the notifier edits in place, so the thread stays current without waking the Planning Agent and without notifying the space once per note (deploy/docker/patches/kanban_progress_lines.py). Child cards are reserved for genuine delegation and parallel work — each one it creates inherits the chat subscription of its own card, so their completions stay visible too. Its one-line routing description for the Planning Agent’s roster lives in agents/platform/CAPABILITIES.md.

  • Automation first. All infrastructure changes route through a declarative workflow — Git PRs, Config Connector, ArgoCD/Flux, whichever is active. The agent is explicitly forbidden from applying YAML directly for infrastructure lifecycle changes.
  • Dynamic multi-repository resolution. On startup, the agent reads the configured GitOps repo URLs from the ConfigMap specified in $GITOPS_STATE_CONFIGMAP. No hardcoded single-repo assumptions.
  • Dynamic project resolution. The agent reads its GCP project, location, and cluster from $GKE_PROJECT_ID / $GKE_LOCATION / $GKE_CLUSTER_NAME, which the operator injects from the PlatformAgent resource. No hardcoded project IDs — and never - in the project segment of a resource path, which GKE accepts only as the location wildcard.
  • Continuous expertise. The agent pulls the latest GitOps repo contents and maintains an expert-level understanding of every declarative definition in the fleet.
  • Grounded, current knowledge. Resolves GKE and Kubernetes specifics (APIs, schemas, versions, deprecations, configuration semantics) against mcp-developer_knowledge (search_documents, then get_documents for a full page) before relying on memory or web search. It does not call answer_query, whose quota is 50 requests a day per project, shared by every agent in the install.
  • Security through strict separation. Tenant isolation is non-negotiable — namespaces, RBAC, NetworkPolicy, ResourceQuota. A workload is physically constrained to its allocated namespace.
  • Least privilege. The agent’s Kubernetes identity is read-only and cannot read Secrets. Its GCP identity is governed by an install-time permission set (read-only by default; widening it means choosing custom and naming every role) — see Security & IAM for exactly what is enforced on which plane.
  • Autonomous recovery. Retries transient auth/IAM/identity failures via a bounded ladder (5 iterations or ~10 minutes per distinct blocker) before escalating to a human.
  • User intent priority. “Fix it for me”, “directly”, “do it”, “loop until done” are permission-granting phrases — the agent proceeds without confirmation. Destructive or irreversible operations (cluster deletion, tenant offboarding, broad IAM revocation) still require explicit human sign-off no matter what phrasing is used.
  • Proactive stance. The agent doesn’t wait to be asked. It surfaces drift, version skew, security baseline violations, IaC/live divergence, and policy gaps — and proposes fixes through the declarative workflow, for what it finds on its own. A question gets its answer, fix included, in the reply; the pull request is opened when you ask for it.

The persona runs inside the Platform Agent Deployment on top of the Hermes runtime (nousresearch/hermes-agent). The wiring lives in agents/platform/config.yaml.

Server Where Purpose
platform_control In-pod, agents/platform/scripts/platform_mcp_server.py Session and agent-internal ops (chat ingress now lives with the Planning Agent).
developer_knowledge Remote via mcp-remote and the credential proxy → developerknowledge.googleapis.com/mcp Authoritative Google Cloud and GKE documentation, API schemas, and best practices.
gke Remote via mcp-remote and the credential proxy → container.googleapis.com/mcp Kubernetes/GKE cluster reads; the server’s write tools are not offered.

The gke MCP server reaches Google’s remote MCP endpoint for GKE through the credential proxy, which forwards on its own credential and relays only the server’s read-only tools; each profile also registers only those read tools. Cluster reads go through a first-class MCP interface rather than shelling out to kubectl or gcloud; cluster changes go through a pull request.

config.yaml groups the servers into toolsets:

  • cli — used by the Hermes CLI (interactive terminal usage).
  • api_server — used by the Hermes REST API (Chat, external callers).

Both include hermes-cli/hermes-api-server plus mcp-platform_control, mcp-developer_knowledge, and mcp-gke.

A top-level toolsets: [kanban] key additionally exposes the kanban orchestrator surface (kanban_create and friends), so the Platform Agent can itself create and route cards when it delegates or stages work.

  • hermes_otel — OpenTelemetry export to the GKE Managed OTel collector.
  • tool_call_audit — appends every tool call and approval decision as a structured audit record to the profile’s logs/audit.jsonl, which the fluent-bit sidecar ships to Cloud Logging as fields (Concepts → Observability).
  • incident_context — injects Kubernetes incident context into known chat threads on reply.
  • pr_opened — records each pull or merge request the Platform Agent’s kanban workers open on their card, so a Slack thread gets it as a message of its own when KAGE_SLACK_UX is on (ChatOps).

The chat-ingress plugins (session_store, session_otel_bridge, chat_metrics) run on the Planning Agent profile, which owns chat ingress — see agents/chat/config.yaml.

  • Delegation-first for single-cluster runtime work. SOUL.md §6 makes the Platform Agent the fleet orchestrator, not a per-workload operator: work scoped to one cluster’s live runtime (crash loops, OOMs, scheduling failures) is delegated to that cluster’s read-only Cluster Agent over the kanban board. Fleet-wide audits, provisioning, and the GitOps write path stay with the Platform Agent.
  • Incident triage discipline. SOUL.md §7 codifies how the agent communicates during triage — findings first, concrete evidence, no raw dumps.
  • Human-readable reports. Raw JSON, tool schemas, and CLI exit codes never appear in the agent’s user-facing messages. Console links are built from the shared URL templates in agents/platform/docs/gcp-console-links.md (baked into the image at /opt/defaults/docs/ and inlined in SOUL.md §5).