Skip to content

PlatformAgent fields

Every field of the PlatformAgent resource’s spec and status, generated from the operator’s Go API types. The standard Kubernetes apiVersion, kind and metadata are not listed. A field whose type comes from the Kubernetes API (corev1, metav1, networkingv1, resource.Quantity) names that type and is not expanded; the Kubernetes API reference describes it. Paths read parent.child, with [] for each item of a list and {} for each value of a map. Each description is the opening paragraph of the field’s documentation, plus its deprecation notice when it has one, and names other fields in their Go spelling (MaxSessions for maxSessions); kubectl explain platformagent.spec.<field> (platformagent.status.<field> for a row of the Status table) prints the whole text. The Constraints column lists the schema bounds on a field: allowed values, ranges, lengths and patterns. The API server also enforces CEL rules on these fields and on whole sections, which the column does not show; kubectl get crd platformagents.kubeagents.x-k8s.io -o yaml lists them under x-kubernetes-validations, each with the message the API server returns when a value breaks it. For what the fields do together, read the PlatformAgent CRD guide.

Field Type Required Default Constraints Description
deployment object no Deployment abstracts the Kubernetes Pod/Deployment configuration.
deployment.image string no Image specifies the container image repository.
deployment.tag string no Tag specifies the container image tag. It applies only when Image is set without a tag or digest, and falls back to “latest” there. When Image is omitted entirely, the operator’s default platform-agent version applies instead, so no “latest” default is persisted on the CR.
deployment.imagePullPolicy corev1.PullPolicy no IfNotPresent one of Always, Never, IfNotPresent ImagePullPolicy specifies if the image should be pulled.
deployment.imagePullSecrets []corev1.LocalObjectReference no ImagePullSecrets references Secrets in the agent’s namespace holding registry credentials, for installs whose mirror needs authenticating to (Harbor, Artifactory) rather than being readable with the nodes’ own credentials. The Secrets are referenced, not created: each must already exist in the agent’s namespace when the pod is scheduled.
deployment.browserArgs []string no BrowserArgs specifies custom command-line arguments to pass to the agent’s browser (e.g. –no-sandbox).
deployment.env []corev1.EnvVar no Env is a list of environment variables to set in the container
deployment.initContainers []corev1.Container no InitContainers specifies standard Kubernetes initContainers to run before the agent starts. A volumeMounts entry naming a reserved volume is refused at admission, and dropped from the render on an install running the A2A surface.
deployment.sidecars []corev1.Container no Sidecars specifies standard Kubernetes sidecar/application containers to run alongside the agent. A volumeMounts entry naming a reserved volume is refused at admission, and dropped from the render on an install running the A2A surface.
deployment.sidecarVolumes []corev1.Volume no SidecarVolumes specifies custom volumes to mount for the sidecar containers. An entry is refused at admission if it takes a reserved volume name, or if its source is one of the routes to the agent’s A2A bus credentials: a ServiceAccount token projection for the “a2a-bus” audience, or a reference to one of the Secrets the operator renders bus credentials into (<agent>-a2a-nats-creds, <agent>-a2a-nats-config, <agent>-a2a-callout-keys, <agent>-a2a-nats-tls, <agent>-a2a-nats-ca, <agent>-a2a-task-signing-key), as either a secret volume or a projected secret source. The refusal is on the name and the shape of the source, not on what a given Secret happens to hold at the time. Reading those Secrets through env is not refused here. On an install running the A2A surface the same entries are dropped from the render as well, which is the half that holds when admission does not run: a default chart install does not register the webhooks at all (operator.webhooks.enabled=false), and one that does registers them at failurePolicy Ignore by default.
deployment.extraVolumes []corev1.Volume no ExtraVolumes specifies custom volumes to mount for the main container. The same reserved names and reserved sources as SidecarVolumes are refused at admission – a ServiceAccount token projection for the “a2a-bus” audience, or a reference to <agent>-a2a-nats-creds, <agent>-a2a-nats-config, <agent>-a2a-callout-keys, <agent>-a2a-nats-tls, <agent>-a2a-nats-ca or <agent>-a2a-task-signing-key – and dropped from the render on an install running the A2A surface.
deployment.extraVolumeMounts []corev1.VolumeMount no ExtraVolumeMounts specifies custom volume mounts for the main container. Appended to platform-agent and platform-agent-dashboard both, so an entry naming a reserved volume is refused at admission, and dropped from the render on an install running the A2A surface. The render drops one more shape admission does not: an entry naming a user volume that was itself dropped for the credential its source carries.
deployment.podAnnotations map[string]string no PodAnnotations specifies custom annotations to apply to the generated Pod template.
deployment.scaleToZero boolean no ScaleToZero scales the deployment replicas to 0 when true (useful for saving costs during idle periods).
deployment.availability object no Availability configures high availability and scheduling settings for the agent pod.
deployment.availability.replicas integer no min 0 Replicas specifies the desired number of gateway pod replicas. If omitted, defaults to 1. The operator runs the gateway as a single replica: a value above 1 is rendered as 1, and the ReplicasLimited status condition says so. A multi-replica gateway does not start yet, because its leader election has no in-cluster ServiceAccount token to use and the data volumes an install creates at one replica are ReadWriteOnce.
deployment.availability.nodeSelector map[string]string no NodeSelector is a selector which must match a node’s labels for the pod to be scheduled
deployment.availability.tolerations []corev1.Toleration no Tolerations are tolerations for pod scheduling
deployment.availability.affinity corev1.Affinity no Affinity specifies affinity scheduling rules
deployment.availability.runtimeClassName string no RuntimeClassName refers to a RuntimeClass object in the cluster. When set, the operator also pins database.journal_mode: delete in the agent pod’s managed config and the entrypoint converts Hermes’ existing databases (state.db, kanban.db and the stores Hermes opens the same way) out of WAL once, because a sandboxed runtime such as gVisor serves the data volume over a gofer mount that corrupts SQLite’s WAL mode. The session KV store this repository runs beside Hermes is not covered by the pin.
deployment.resources corev1.ResourceRequirements no Resources specifies resource requests and limits for the main container.
deployment.credentialProxy object no CredentialProxy configures the credential-proxy container, the broker that runs every credentialed command on the agent’s behalf in a pod of its own.
deployment.credentialProxy.resources corev1.ResourceRequirements no Resources overrides the credential-proxy container’s requests and limits. Each key set here replaces the operator’s default for that key and the rest keep their defaults, unlike spec.deployment.resources, which replaces the agent container’s block wholesale: a CR that sets only limits.memory keeps the default 500m CPU request, 1 CPU limit and 8Gi ephemeral-storage limit. The broker sizes how many commands it admits at once from the memory limit, so raising the limit is the one knob for an install whose fleet outgrows the default; the ephemeral-storage limit bounds the content workspace the broker clones into; the proxy’s state emptyDir (sizeLimit 5Gi) grows by whatever the limit is raised above the 8Gi default, and its /tmp emptyDir keeps its 2Gi, so the kubelet does not evict the pod at the smaller figure. The limit a fleet needs grows with the size of its clusters as well as their number: the broker charges each command about 128Mi, the size of a listing of a small cluster, and a kubectl listing thousands of objects holds several hundred Mi on its own, so size the limit to the largest listing the fleet produces for every command that runs at once (eight at the slot cap) plus the 320Mi of fixed reserves (192Mi for the broker and Envoy, 128Mi for the content workspace); the broker’s child peak and memory high-water series give the figure. Only cpu, memory and ephemeral-storage are accepted, the quantities the container declares. The operator refuses a memory limit below what admits two commands at once, a limits entry set alone below the default request it would cross, a request above its limit, a negative quantity and a zero limit; claims is refused, because the proxy pod declares no resourceClaims. A refused override, including an edit of one that was valid, renders the proxy Deployment at the operator’s defaults, not at the last accepted override, until it is corrected, which on the proxy’s Recreate Deployment restarts the proxy once; the operator reports Degraded with reason InvalidCredentialProxyResources when no higher-ranked Degraded cause is present, the agent staying Ready, whether or not the validating webhook is enabled. Where the webhook is on, it refuses the edit at apply and the running proxy is untouched. The webhook warns when memory per CPU on the requests pair leaves the band GKE Autopilot admits unchanged, because Autopilot then raises the smaller request into the band; it applies the band to requests only. It also warns for each of cpu and memory set under limits without the same key under requests, unless the limit equals the request it would be replaced by: Autopilot without bursting sets the limits equal to the requests, so there the proxy runs at the request and the limit has no effect. With bursting the declared limits stand.
deployment.agentAPIAuth object no AgentAPIAuth configures the agent-api-auth sidecar in the gateway pod, the native container that authenticates the agent’s API-server calls and also runs the event watcher and the drift detector.
deployment.agentAPIAuth.resources corev1.ResourceRequirements no Resources overrides the agent-api-auth container’s requests and limits. Each key set here replaces the operator’s default for that key and the rest keep their defaults, unlike spec.deployment.resources, which replaces the agent container’s block wholesale: a CR that sets only limits.memory keeps the default 150m CPU request, 384Mi memory request, 1 CPU limit and 2Gi ephemeral-storage limit. The container runs the event watcher, the drift detector and the API authenticator together; the event watcher reads the memory limit through the Downward API and sets its Go soft memory limit to half of it, so raising the memory limit is the knob for an install whose fleet of watched clusters outgrows the 2Gi default. Only cpu, memory and ephemeral-storage are accepted, the quantities the container declares. The operator refuses a request above its limit, a negative quantity, a zero limit, an unrepresentable byte or CPU count and claims, because the gateway pod declares no resourceClaims. A refused override, including an edit of one that was valid, renders the sidecar at the operator’s defaults until it is corrected, and the operator reports Degraded with reason InvalidAgentAPIAuthResources when no higher-ranked Degraded cause is present, the agent staying Ready, whether or not the validating webhook is enabled. Where the webhook is on, it refuses the edit at apply. The webhook warns when memory per CPU on the requests pair leaves the band GKE Autopilot admits unchanged, and when a cpu or memory limit is set without the same key under requests, for the reasons the credential-proxy field documents.
deployment.defaultStorageClassName string no DefaultStorageClassName specifies the default storage class to use for the system and data PVCs.
deployment.storages []object no Storages specifies extra custom PersistentVolumeClaims to provision and mount for the agent pod.
deployment.storages[].name string yes Name specifies the PersistentVolumeClaim name.
deployment.storages[].storageClassName string no StorageClassName specifies the storage class name for this volume claim.
deployment.storages[].accessModes []corev1.PersistentVolumeAccessMode no AccessModes specifies the requested access modes (e.g. ReadWriteOnce, ReadWriteMany).
deployment.storages[].storageSize string no 5Gi StorageSize specifies the requested storage capacity (e.g. 5Gi, 20Gi).
deployment.storages[].mountPath string no MountPath specifies the container mount directory path for this volume claim.
deployment.storages[].subPath string no SubPath specifies a sub-path within the volume to mount.
deployment.storages[].readOnly boolean no ReadOnly specifies if the volume should be mounted as read-only.
security object no Security configures RBAC, Pod Security, and Workload Identity.
security.serviceAccountName string no ServiceAccountName is the Kubernetes Service Account bound to the Deployment.
security.serviceAccountAnnotations map[string]string no ServiceAccountAnnotations specifies custom annotations to apply to the generated ServiceAccount.
security.scopedServiceAccountPool object no ScopedServiceAccountPool maps each GCP project the agent may read to the Google service account that reads it. The credential broker mints a short-lived token for the account a request’s project maps to, instead of using the agent’s own identity — which, holding a project-level roles/container.viewer, can read objects in every cluster in the project.
security.scopedServiceAccountPool.enabled boolean no false Enabled arms the credential broker: with it true, every cluster read is made as the account its project maps to, and a project with no entry is refused. Default false, and it should stay false until the pool’s accounts hold authority.
security.scopedServiceAccountPool.serviceAccounts []object no ServiceAccounts is the mapping, one entry per project.
security.scopedServiceAccountPool.serviceAccounts[].projectId string yes pattern ^[a-z0-9][a-z0-9-]*$; max length 63 ProjectID is the project whose clusters this account reads, which need not be the project the agent runs in.
security.scopedServiceAccountPool.serviceAccounts[].serviceAccountEmail string yes pattern ^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z0-9-]{6,30}\.iam\.gserviceaccount\.com$ ServiceAccountEmail is the account scoped to this project. It lives in the host project, whatever project it reads; Terraform’s scoped_service_accounts output is the source of these values.
security.workloadIdentityFederation object no WorkloadIdentityFederation gives the credential proxy a GCP identity that does not come from the metadata server.
security.workloadIdentityFederation.audience string no max length 512; pattern ^//iam\.googleapis\.com/projects/[0-9]+/locations/global/workloadIdentityPools/[^/]+/providers/[^/]+$ Audience is the provider’s full resource name, in the form STS expects as the aud claim.
security.workloadIdentityFederation.serviceAccountEmail string no max length 256 ServiceAccountEmail is the GSA the federated principal impersonates.
security.egressPolicy string no one of None, Allowlist EgressPolicy selects the NetworkPolicy the operator renders for the agent Pod. “None” (the default) renders nothing.
security.egressAllowlist object no EgressAllowlist tunes the destinations egressPolicy: Allowlist permits. Ignored for any other egressPolicy value.
security.egressAllowlist.controlPlaneCIDRs []string no ControlPlaneCIDRs are the address ranges of the Kubernetes API server, permitted on port 443.
security.egressAllowlist.extraRules []networkingv1.NetworkPolicyEgressRule no ExtraRules are appended verbatim to the rendered policy, for destinations a plugin or a custom sidecar needs.
telemetry object no Telemetry configures OpenTelemetry export for this agent.
telemetry.otlpEndpoint string no max length 2048; pattern ^$|^https?://[^\s]+$ OTLPEndpoint is the base URL of an OTLP/HTTP collector, for example “http://otel-collector.otel-collector.svc.cluster.local:4318”. Give the base URL only — the per-signal path (“/v1/traces”) is appended by the exporter.
networkPolicy object no NetworkPolicy configures the operator-generated egress NetworkPolicy.
networkPolicy.enabled boolean no Enabled turns operator-managed NetworkPolicy generation off entirely, for installs that manage network policy through their own tooling. Unset means on.
networkPolicy.dnsClusterIPs []string no max items 8; items max length 45; items pattern ^((((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]))|(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}))$ DNSClusterIPs pins the cluster DNS Service ClusterIPs. Setting it disables discovery, like spec.telemetry.otlpEndpoint. Each entry is a bare IP with no prefix; the operator writes it into rule 1 as a /32 or /128.
networkPolicy.metadataDaemon object no MetadataDaemon describes the node-local cloud metadata daemon. Leave nil to let the operator discover the container port from the kube-system/gke-metadata-server DaemonSet (falling back to port 988 and 169.254.169.252). Overriding the endpoint via annotation, spec, or operator flag opts out of discovery and uses port 988. Present with Endpoint “” emits no post-NAT rule at all, for datapaths that evaluate pre-NAT or clouds without one.
networkPolicy.metadataDaemon.endpoint string yes pattern ^($|(((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]))|(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}))$; max length 45 Endpoint is the daemon IP. “” (explicitly set) suppresses that rule entirely; the empty alternative in the pattern is required because the API server validates an explicitly-set “”, which omitempty does not suppress.
networkPolicy.additionalEgress []object no max items 32 AdditionalEgress appends CIDR-and-port egress rules to the generated policy. Entries are not passed through untouched: every peer CIDR is canonicalised, and three things the schema below cannot express are dropped by the operator instead – an IPv4-mapped IPv6 peer, which clears the IPv6 prefix floor and then fails the IPv4 one once collapsed to the block it means; an except that is not a strict subset of its peer, which the API server would reject the whole policy for; and a rule left with no usable peer, which would otherwise permit egress to every destination. Each drop is logged and costs only the entry it names. Everything else is rejected at admission – except an entry with no ports, which is admitted and opens every port to its peers. See EgressRule.ports.
networkPolicy.additionalEgress[].to []object yes min items 1; max items 16 EgressPeer defines a CIDR block and optional exclusions.
networkPolicy.additionalEgress[].to[].cidr string yes min length 1; max length 49; pattern ^((((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(/(1[2-9]|2[0-9]|3[0-2]))?)|(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}(/(4[89]|[5-9][0-9]|1[01][0-9]|12[0-8]))?))$ CIDR is an IPv4/IPv6 block or host IP, e.g. 10.0.0.0/24 or 10.0.0.1.
networkPolicy.additionalEgress[].to[].except []string no max items 16; items max length 49; items pattern ^((((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(/([0-9]|[12][0-9]|3[0-2]))?)|(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}(/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?))$ Except carves ranges out of CIDR. Each entry must be a strict subset of CIDR – contained by it and narrower than it – because ValidateIPBlock rejects the whole NetworkPolicy otherwise, which would freeze every other egress rule at its previous revision. The resolver applies the same test and drops an except that fails it rather than forwarding it.
networkPolicy.additionalEgress[].ports []object no max items 16 Ports restricts the rule to these destination ports. Omitting it emits a rule with peers and no ports, which in NetworkPolicy semantics permits EVERY port to those peers – the mirror of the case the operator refuses to emit, a rule with ports and no surviving peer. That is standard NetworkPolicy behaviour and a legitimate thing to ask for, so it is admitted rather than blocked and nothing is logged; list the ports if you did not mean it.
networkPolicy.additionalEgress[].ports[].protocol string yes one of TCP, UDP, SCTP
networkPolicy.additionalEgress[].ports[].port integer yes min 1; max 65535
harness object yes Harness configures the core execution environment and framework-level settings.
harness.clusterName string yes ClusterName is the logical name of the cluster (either where the agent is running or the target cluster).
harness.location string yes Location is the geographical location or cloud region.
harness.projectId string yes ProjectID is the GCP Project ID of the cluster. Required alongside ClusterName and Location: the credential proxy only renders its bootstrap (the gcloud container clusters get-credentials that gives the agent a usable kubectl context) when all three are set. Omitting it leaves every kubectl call in the sidecar pointed at localhost:8080. See buildCredentialProxyEnv.
harness.hermes object no Hermes configures the internal event-routing or agent framework.
harness.hermes.dashboardEnabled boolean no true DashboardEnabled toggles the AGENT_DASHBOARD environment variable.
harness.hermes.pluginsDebug boolean no false PluginsDebug toggles the AGENT_PLUGINS_DEBUG environment variable.
harness.hermes.agentHome string no /opt/data AgentHome is the path to the AGENT_HOME directory.
harness.hermes.apiServerSecretRef corev1.SecretKeySelector no ApiServerSecretRef references the Secret key holding API_SERVER_EXTERNAL_KEY, the credential outside callers present to the credential-proxy sidecar. It does not set API_SERVER_KEY: the value the Hermes API server itself validates is the non-secret loopback sentinel cluster-internal-trusted, a compile-time constant the sidecar swaps in once it has authenticated the caller.
harness.hermes.sessionKVApiKeySecretRef corev1.SecretKeySelector no SessionKVApiKeySecretRef references the Secret key holding the bearer token for the pod-local Session KV server on port 8699. Distinct from API_SERVER_KEY, which is that loopback sentinel and would authenticate nothing here.
harness.hermes.sessionKVSaltSecretRef corev1.SecretKeySelector no SessionKVSaltSecretRef references the Secret key holding the HMAC salt used to pseudonymise chat identities before they reach session metadata, audit logs, or OTel spans. When absent the agent generates a per-pod salt and logs a warning: hashes then stop correlating across restarts.
harness.memory object no Memory configures agent memory settings.
harness.memory.memoryEnabled boolean no false MemoryEnabled toggles framework memory persistence.
harness.memory.provider string no multiuser_memory Provider selects the memory provider plugin. Two ship in the agent image: “multiuser_memory” — the default, for small or personal deployments — keeps a per-user Markdown file inside the pod and needs nothing else running, at the price of loading the whole store into the model’s context on every turn, and “kube_agents_memory” — for enterprise deployments — gives ranked recall backed by the in-cluster Hindsight service and its Postgres database. Any other plugin Hermes ships may be named here too.
harness.memory.userProfileEnabled boolean no false UserProfileEnabled toggles per-user memory profiling.
harness.eventWatcher object no EventWatcher configures cluster event ingestion — the k8s-event-watcher that turns cluster warnings into autonomous triage sessions. Its enabled: false is the emergency stop for an event storm.
harness.eventWatcher.enabled boolean no true Enabled controls whether the watcher is started at all. Absent means started: the watcher is how a fleet notices its own incidents, so only an explicit false turns it off.
harness.driftDetector object no DriftDetector configures out-of-band change detection — the drift-detector that reads GKE admin-activity audit records from a Pub/Sub subscription and turns the ones a person made outside git into triage cards. Off unless asked for, unlike EventWatcher above.
harness.driftDetector.enabled boolean no false Enabled controls whether the detector is started. Absent means not started, the opposite of EventWatcher, and the reason is a dependency rather than caution: the detector reads a Pub/Sub subscription that exists only where the drift-pubsub Terraform module was applied. An install without one that started the detector anyway would get a process that never exits and never reports a change — the subscription is not checked at startup, and a pull that fails because it does not exist is retried for the life of the pod. The pod stays Ready, so an install left on by accident is indistinguishable from a fleet nobody has touched.
harness.driftDetector.subscription string no Subscription is the Pub/Sub subscription carrying the audit records. Empty takes the detector’s own default, which is the name the drift-pubsub module creates; set it only for a subscription made by hand or renamed.
harness.driftDetector.gitopsManagers string no GitopsManagers names the managedFields managers that are the GitOps controller — argocd-controller, flux. Comma-separated and matched exactly.
harness.stallWatch object no StallWatch tunes the controller stall watch — the stall-watch cron job on the Platform Agent’s roster that sweeps the fleet for controllers that stopped making progress and raises an alert for each new stall. Absent keeps the watch’s built-in behaviour.
harness.stallWatch.excludeNamespaces []string no max items 100; items max length 63; items pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ExcludeNamespaces names namespaces the watch never reads, on every cluster, in addition to the system namespaces it always skips. Matched exactly. An open alert in a namespace added here is closed with a note that the namespace was excluded.
harness.stallWatch.minStallAgeMinutes integer no min 0; max 1440 MinStallAgeMinutes holds a new alert until one of the namespace’s stalls has lasted at least this long. The age is the longer of what stall_report.py measures on the object and how long the watch has seen it. A stall that is younger stays in the watch’s ledger and is checked again on each run; an object that joins a namespace with an open alert is not held. Absent or 0 alerts on the first run that sees the stall.
harness.stallWatch.kinds []string no max items 20; items max length 253; items pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ Kinds replaces the watch’s default list of resources to scan (Deployments, StatefulSets, DaemonSets, Jobs, Gateway API Gateways and HTTPRoutes, and cert-manager Certificates). Each item is a kubectl resource name such as deployments or gateways.gateway.networking.k8s.io. The single item all scans every namespaced kind the cluster serves, one read per kind per namespace, which is slow on a large fleet. Absent or empty keeps the default list. Secrets, ConfigMaps, Events, Endpoints, EndpointSlices, ControllerRevisions and Leases are refused: the scan reports nothing from them, and a Secret listing would carry every body.
harness.tuning object no Tuning sets per-persona execution limits. Unset values keep the defaults baked into the agent image.
harness.tuning.default object no Default applies to the default profile — the Chat Agent front door. Delivered as a config overlay merged into that profile at pod startup, like the others.
harness.tuning.default.apiMaxRetries integer no min 1; max 100 APIMaxRetries is how many times a failed model call is retried before the run gives up. Hermes defaults to 3, which suits an interactive session where a human can retry; a background worker has nobody to retry it, so a transient burst of upstream 429s or 503s ends the run.
harness.tuning.default.maxTurns integer no min 1; max 1000 MaxTurns is how many iterations (model calls) a single turn may take. Hermes defaults to 90. A long multi-step task can exhaust it while still mid-flight, and a run that does cannot even produce a closing summary. Repository exploration is the main consumer, so size this against how much the agent has to read, not against how complex the request is.
harness.tuning.platform object no Platform applies to the platform profile (the Platform Agent). Delivered as a config overlay merged into that profile at pod startup.
harness.tuning.platform.apiMaxRetries integer no min 1; max 100 APIMaxRetries is how many times a failed model call is retried before the run gives up. Hermes defaults to 3, which suits an interactive session where a human can retry; a background worker has nobody to retry it, so a transient burst of upstream 429s or 503s ends the run.
harness.tuning.platform.maxTurns integer no min 1; max 1000 MaxTurns is how many iterations (model calls) a single turn may take. Hermes defaults to 90. A long multi-step task can exhaust it while still mid-flight, and a run that does cannot even produce a closing summary. Repository exploration is the main consumer, so size this against how much the agent has to read, not against how complex the request is.
harness.tuning.cluster object no Cluster applies to every cluster-* profile (the Cluster Agents). Delivered as a single class overlay, merged into each existing cluster profile at pod startup and into a new one when it is scaffolded — onboarding a cluster does not roll the pod, so a profile created between two starts has to pick the overlay up itself.
harness.tuning.cluster.apiMaxRetries integer no min 1; max 100 APIMaxRetries is how many times a failed model call is retried before the run gives up. Hermes defaults to 3, which suits an interactive session where a human can retry; a background worker has nobody to retry it, so a transient burst of upstream 429s or 503s ends the run.
harness.tuning.cluster.maxTurns integer no min 1; max 1000 MaxTurns is how many iterations (model calls) a single turn may take. Hermes defaults to 90. A long multi-step task can exhaust it while still mid-flight, and a run that does cannot even produce a closing summary. Repository exploration is the main consumer, so size this against how much the agent has to read, not against how complex the request is.
harness.tuning.maxInProgress integer no min 1 MaxInProgress caps how many kanban workers run concurrently across the whole board. It is board-wide rather than per-persona: there is one dispatcher, and every worker it spawns — platform and cluster alike — draws on the same model quota. Setting it to 1 serialises all delegated work.
harness.tuning.maxSessions integer no min 1; max 10000 MaxSessions caps how many A2A session pods run concurrently, install-wide. It only means something under mode: next - a today install renders neither the gateway that spawns session pods nor this bound. “Delegate:” makes pod creation user-triggerable from chat and threads are free, so the principal map bounds WHO can spawn and this bounds HOW MANY.
harness.tuning.profiles object no Profiles sizes AgentProfile work: the install-wide ceiling on concurrent profile tasks and the warm workers kept per interactive (profile, tier). Until the sizing budget exists these are fixed, conservative defaults.
harness.tuning.profiles.ceiling integer no min 1; max 1000 Ceiling is the most profile tasks running at once across every profile on the install, of every kind. The dispatcher keeps two disjoint reserves in it, ChildReserve for child tasks (so waiting parents can’t hold every slot) and max(1, ceil(Ceiling/3)) for interactive tiers (so unattended work can’t starve people), trimmed child first to leave at least one slot for top-level unattended work. With the default reserves, ceilings of 1, 2 and 4 can’t honour both in full, and the PlatformAgent warns. The profile namespace’s pod quota is derived from it. Absent means maxInProgress’s resolved value, so an install’s footprint doesn’t jump while both exist (spec-profiles-dispatch.md).
harness.tuning.profiles.childReserve integer no min 1; max 999 ChildReserve is the share of Ceiling kept for child tasks, which top-level tasks can’t take. A parent holds its slot while its children run, so without it a ceiling full of waiting parents could never start their children. Absent means max(1, ceil(Ceiling/3)), trimmed to Ceiling-1 so one slot stays unreserved (none at a ceiling of 1).
harness.tuning.profiles.leaseSeconds integer no min 15; max 3600 LeaseSeconds is how long a task’s liveness lease lasts without a renewal before the dispatcher fails the task (it is never re-run). Absent means DefaultProfileLeaseSeconds.
harness.tuning.profiles.renewSeconds integer no min 5; max 300 RenewSeconds is how often a worker renews each task’s lease; the dispatcher sends it with every offer. Absent means DefaultProfileRenewSeconds.
harness.tuning.profiles.restartGraceSeconds integer no min 10; max 3600 RestartGraceSeconds is how long a restarted dispatcher waits, from its own start, for a renewal on each task it re-adopts before failing it. While the dispatcher is down nobody reads renewals, so this is what keeps a redeploy from failing every task in flight. Absent means DefaultProfileRestartGraceSeconds.
harness.tuning.profiles.acceptTimeoutSeconds integer no min 1; max 120 AcceptTimeoutSeconds is how long the dispatcher waits for a worker to accept an offer before treating it as declined. Absent means DefaultProfileAcceptTimeoutSeconds.
harness.tuning.profiles.maxTaskDeadlineSeconds integer no min 60; max 21600 MaxTaskDeadlineSeconds caps the deadline any profile task gets: a profile whose lifecycle.activeDeadlineSeconds is longer has its tasks clamped to this, with a warning. Once profile tasks get their own bus credentials it is also the longest a task’s credential may live, which the auth callout enforces, bounding a leaked one; the operator renders it to the callout already. It may only be lowered from the default. Absent means DefaultProfileMaxTaskDeadlineSeconds (6h).
harness.tuning.profiles.warmReplicas integer no min 0; max 100 WarmReplicas is how many warm worker pods each interactive (profile, tier) keeps running. Absent means DefaultProfileWarmReplicas.
harness.retention object no Retention bounds how long the agent keeps its own working data: the session history in every profile’s state.db and its rotated log files. Absent keeps the operator’s default.
harness.retention.sessionDays integer no min 90; max 365 SessionDays is how many days of inactivity an ended session is kept before Hermes’ own pruning deletes it, with its messages and transcript files, in every profile: the front door’s conversations, the Platform Agent’s, and each Cluster Agent’s. Rotated log backups older than this are removed too. Open, pinned and mid-turn sessions are never pruned, and the daily job pins a session another still points to as its parent before it prunes. Unset uses the operator’s default (defaultSessionRetentionDays in the controller, 90). The minimum is 90 for now: some readers follow a session’s lineage further than the parent guard has been shown to hold below that. Lowering it deletes the older history at the next daily prune.
harness.experimental object no Experimental holds opt-in behaviour that is not supported and may change or disappear in any release.
harness.experimental.platformFrontDoor boolean no false PlatformFrontDoor makes the Platform Agent the profile the Hermes gateway runs as, so chat messages are handled by it directly instead of arriving at the Chat Agent, which delegates through the router and the kanban board.
harness.experimental.shellSandbox object no ShellSandbox tunes the agent’s shell sandbox — the separate pod it reaches over SSH, where every command it runs executes. The sandbox itself is not optional; this block sets its image and its container runtime. See docs/designs/agent-shell-sandboxing.md.
harness.experimental.shellSandbox.enabled boolean no Enabled is retained so that an install carrying enabled: true still applies, and so that enabled: false is refused rather than silently ignored. The sandbox is not optional: the agent image ships no kubectl, gcloud, gh or git, so an agent without a sandbox has no shell tools at all, and running them back in the gateway pod is the arrangement this design exists to end. Setting it false parks the agent Degraded with reason ShellSandboxCannotBeDisabled and changes nothing about the running workload.
harness.experimental.shellSandbox.image string no Image overrides the sandbox image. Empty takes the operator’s own default, which the install surfaces set from the same tag inventory as every other image (AGENT_SANDBOX_IMAGE).
harness.experimental.shellSandbox.runtimeClassName string no RuntimeClassName runs the sandbox pod under a sandboxed container runtime, gvisor being the one GKE offers. This is a second boundary and not the one the sandbox is built on: unbinding the ServiceAccount is what takes the cloud credential away, and a shell running as a different pod is what takes the agent’s filesystem away. What this adds is protection of the node from the code the model runs, by putting a user-space kernel between that code and the host’s syscall surface.
harness.experimental.shellSandbox.resources corev1.ResourceRequirements no Resources overrides the shell container’s requests and limits. Each key set here replaces the operator’s default for that key and the rest keep their defaults, as spec.deployment.credentialProxy.resources and spec.deployment.agentAPIAuth.resources do and unlike spec.deployment.resources, which replaces the agent container’s block wholesale: a CR that sets only requests.memory keeps the default 200m CPU request, 2 CPU limit and 2Gi memory limit. The shell is where every model-authored command runs, so a fleet sweep that fans out many kubectl calls at once is bounded by this container’s memory; on GKE Autopilot without bursting the limits are set equal to the requests, so there requests.memory is the figure to raise when the shell pod is OOM-killed, with limits.memory to match. Only cpu, memory and ephemeral-storage are accepted: the container declares cpu and memory, and an ephemeral-storage limit set alone becomes its request by the API server’s defaulting. The operator refuses a request above its limit, a negative quantity, a zero limit, an unrepresentable byte or CPU count and claims, because the sandbox pod declares no resourceClaims. A refused override, including an edit of one that was valid, renders the shell at the operator’s defaults until it is corrected, and the operator reports Degraded with reason InvalidShellSandboxResources when no higher-ranked Degraded cause is present, the agent staying Ready, whether or not the validating webhook is enabled. Where the webhook is on, it refuses the edit at apply. The webhook warns when memory per CPU on the requests pair leaves the band GKE Autopilot admits unchanged, and when a cpu or memory limit is set without the same key under requests, for the reasons the credential-proxy field documents. A change rolls the shell pod, as an image change does. The image-trees init container, and the gateway pod’s sandbox-ssh-key init container and sandbox-credential-cleanup sidecar, keep their own fixed figures.
integration object no Integration configures platform-specific external connections.
integration.forges []object no max items 16 Forges declares the forges this agent works with: which provider each one is, where it is, and which organisation it acts for. Repositories name a forge from this list.
integration.forges[].name string yes min length 1; max length 63; pattern ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ Name identifies the forge within this PlatformAgent. Repositories refer to it by this name. The deprecated GitHub alias is the forge “github”.
integration.forges[].provider string no one of github, gitlab, gitea Provider names the forge’s kind. It is the discriminator the operator writes into the gitops-state ConfigMap as each repository’s type, so the agent reads which forge was declared rather than guessing from the URL’s text.
integration.forges[].host string no max length 253; pattern ^$|^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$ Host is the forge hostname. Omit it for the provider’s default (“github.com” for GitHub, “gitlab.com” for GitLab). A host the declared provider does not serve is rejected, and an alternative spelling of one it does serve resolves to the provider’s canonical host. It may also be a self-managed instance’s hostname: a GitLab instance, a Gitea instance, or a GitHub Enterprise Server instance. Every repository on the forge must then name that host or leave it implied. A self-managed provider such as “gitea” has no default, so the host is required there. A host another provider serves is rejected, and so is a host under a domain where the provider runs its own services (for GitHub: github.com, githubusercontent.com, ghe.com and others).
integration.forges[].namespace string no max length 255; pattern ^$|^[A-Za-z0-9][A-Za-z0-9._/-]*$ Namespace is the organisation, user, or group path the agent acts for on this forge — the GitHub org that GitHubSpec called Org. A repository given as a bare name is qualified by it, so without it every repository on this forge must name its namespace. If omitted, the organisation the token minter and GITHUB_ORG use is read from the first accepted repository the agent writes to on this forge, the GitOps one first; bare repository names are not qualified by it.
integration.forges[].scheme string no one of https, http Scheme is how the forge is reached: “https”, the default, or “http”. Only a provider that allows plain http accepts it, and only when it is written out; GitHub is https only. Over http the token and repository contents cross the network unencrypted, so admission warns, and it is meant for a forge inside the cluster.
integration.forges[].port integer no min 1; max 65535 Port is the forge’s port, for a forge not on the scheme’s default port. Only provider “gitea” accepts it; it is refused for every other provider, a self-managed GitLab included, whose broker client does not compose one.
integration.forges[].credentialsRef object no CredentialsRef names a Secret in the PlatformAgent’s namespace holding the credentials for this forge. It is for providers whose credentials an administrator supplies, and it is required for them. GitHub’s come from the install’s GitHub App through the token minter, so it is ignored for provider “github”, and admission warns when it is set there.
integration.forges[].credentialsRef.name string yes min length 1; max length 253; pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ Name is the Secret’s name: a lowercase DNS subdomain.
integration.forges[].caBundleRef object no CABundleRef names a Secret in the PlatformAgent’s namespace that holds the PEM CA certificates that signed this forge’s TLS certificate: a self-managed instance behind a private CA. The broker trusts these certificates for this forge’s host only, beside the system bundle, in its API client and in git. On a GitHub Enterprise Server forge, the chart also gives them to the token minter, which trusts them for all of its outbound calls. It is for a forge at a self-managed host, GitLab or GitHub Enterprise Server: it is refused when host is empty, gitlab.com or github.com, whose certificates the public CAs sign.
integration.forges[].caBundleRef.name string yes min length 1; max length 253; pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ Name is the Secret’s name: a lowercase DNS subdomain.
integration.forges[].caBundleRef.key string no ca.crt max length 253; pattern ^[-._a-zA-Z0-9]+$ Key is the Secret key that holds the PEM certificates. Defaults to “ca.crt”.
integration.repositories []object no max items 64 Repositories declares the repositories this agent works with, each on one of Forges, and what the agent does with it. The operator seeds them into the gitops-state ConfigMap: the GitOps repository and the managed ones into managed_repos, in that order, and the context ones into context_repos. Seeding only adds; an entry removed here stays in the ConfigMap until an administrator removes it there too.
integration.repositories[].forge string yes min length 1; max length 63 Forge is the name of the entry in Forges this repository lives on.
integration.repositories[].repository string yes min length 1; max length 2048 Repository is a clone URL, an scp-style remote, a namespace-qualified path, or a bare name to be qualified by Namespace. A URL or remote must name one of the forge’s own hosts.
integration.repositories[].namespace string no max length 255; pattern ^$|^[A-Za-z0-9][A-Za-z0-9._/-]*$ Namespace qualifies a bare repository name, overriding the forge’s namespace for this repository only.
integration.repositories[].role string yes one of gitops, managed, context Role is what the agent does with the repository: “gitops” for the repository its GitOps work lands in, “managed” for a further repository it writes to, “context” for one it only reads.
integration.repositories[].baseBranch string no max length 200; pattern ^$|^[A-Za-z0-9][A-Za-z0-9._/-]*$ BaseBranch is the branch every pull request onto this repository must target. The credential broker enforces it: it refuses a proposal onto any other branch, and a clone that names no branch checks it out. Empty means the repository’s own default branch. It may be set on a “gitops” or a “managed” repository, not on a “context” one, which is never written and whose branch pin is the ref in the gitops-state ConfigMap. The deprecated GitHub alias has no place for it: pinning the GitOps repository’s base takes Forges and Repositories.
integration.github object no GitHub configures the GitHub integration. Deprecated: use Forges and Repositories, which name their forge. This field is kept as an alias: it means one forge named “github” with provider “github” and namespace Org, and, when GitRepo is set, one repository on it with role “gitops”. It will be removed in a future API version.
integration.github.org string no max length 39; pattern ^$|^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$ Org is the target GitHub organization or user account for the agent environment. If omitted and GitRepo is provided, the organization is inferred from the repository owner.
integration.github.gitRepo string no max length 2048 GitRepo is the optional target GitOps repository URL or owner/repo shorthand for the agent environment. When omitted or empty, no repository is initially configured, and repositories can be registered in the gitops-state ConfigMap by a cluster administrator.
integration.googleChat object no GoogleChat configures the Google Chat integration.
integration.googleChat.enabled boolean no false Enabled toggles the Google Chat integration.
integration.googleChat.projectId string no ProjectID is the target GCP Project ID for Pub/Sub.
integration.googleChat.topicName string no TopicName is the GCP Chat Topic Name.
integration.googleChat.subscriptionName string no SubscriptionName is the GCP Chat Subscription Name.
integration.googleChat.allowedUsers []string no AllowedUsers is a list of allowed users. If not present, all users will be allowed.
integration.googleChat.homeChannel string no HomeChannel is the home channel Chat address.
integration.googleChat.mode string no default one of default, debug Mode controls output verbosity in Google Chat (“default” or “debug”). “default”: Quiet mode (silences memory reviews, approval cards, and tool progress). “debug”: Full verbosity (surfaces tool progress, memory reviews, interim messages, and approval cards).
integration.slack object no Slack configures the Slack integration.
integration.slack.enabled boolean no false Enabled toggles the Slack integration.
integration.slack.botTokenSecretRef corev1.SecretKeySelector no BotTokenSecretRef securely references a Secret containing the SLACK_BOT_TOKEN.
integration.slack.appTokenSecretRef corev1.SecretKeySelector no AppTokenSecretRef securely references a Secret containing the SLACK_APP_TOKEN.
integration.slack.allowedUsers []string no AllowedUsers is a list of allowed member IDs. If not present, all users will be allowed. A plain ID (U…) is a member of the install’s own workspace. Under mode next, an entry T…:U… (a workspace ID, a colon, a member ID) admits that member of another workspace in a Slack Connect channel; nothing else admits one, an absent list included.
integration.slack.homeChannel string no HomeChannel is the default channel ID for scheduled messages.
integration.slack.homeChannelName string no HomeChannelName is the human-readable name for the home channel.
integration.slack.answerFeedback boolean no true AnswerFeedback draws Slack’s thumbs-up and thumbs-down buttons under each final answer under spec.mode next, and keeps each rating with its question and answer (a thumbs-down with its note) on the install for the feedback bucket’s max age (30 days as provisioned). False hides the buttons and keeps nothing.
integration.slack.homeChannelPolicy string no one of auto, manual HomeChannelPolicy (“auto”, also absent, or “manual”) is accepted and recorded in this release but has no effect yet: the A2A gateway does not learn a home channel, so HomeChannel alone decides. It is rendered to the gateway as A2A_SLACK_HOME_POLICY for a gateway that learns the channel of the first admitted mention under “auto”. No schema default: the API server would apply one to every stored object it reads, so absent is read as “auto” by the operator instead.
integration.teams object no Teams configures the Microsoft Teams integration.
integration.teams.enabled boolean no false Enabled toggles the Microsoft Teams integration.
integration.teams.appIdSecretRef corev1.SecretKeySelector no AppIdSecretRef securely references a Secret containing the Microsoft App (Client) ID.
integration.teams.appPasswordSecretRef corev1.SecretKeySelector no AppPasswordSecretRef securely references a Secret containing the Microsoft App Client Secret.
integration.teams.tenantId string no TenantId is the Microsoft Entra ID Tenant ID for single-tenant enterprise lock-down.
integration.teams.allowedUsers []string no AllowedUsers is a list of allowed Entra ID Object IDs (from.aadObjectId) or Bot Framework user IDs (from.id). An inbound Teams activity carries no User Principal Name or email. When empty and allowAllUsers is false (default), interactions are rejected.
integration.teams.allowAllUsers boolean no false AllowAllUsers allows any authenticated tenant user to interact with the bot. When false (default), users not listed in allowedUsers are rejected.
integration.teams.homeChannel string no HomeChannel is the default channel ID for scheduled messages.
integration.teams.homeChannelName string no HomeChannelName is the human-readable name for the home channel.
integration.teams.adaptiveCards boolean no true AdaptiveCards toggles rich Adaptive Card rendering for reports and updates.
mode string no one of today, next Mode selects which component stack the operator renders. “today” is the current architecture. “next” additionally renders the NATS and A2A gateway components, which are otherwise dark. Absent means “today”.
scope object no Scope declares which GCP projects, beyond the one the agent runs in, the Cluster Agent reconcile enumerates for GKE clusters, and which projects and clusters it leaves unmanaged. Absent, the reconcile lists the management project alone, keeps the last declaration’s exclusions and retires nothing; an empty projects list in a present block drops the projects an earlier block declared. The management project is always in scope and cannot be excluded. The design is docs/designs/multi-project-scope.md; this is its phases 1 to 3: explicit projects, folders and organisations, and the sharedVpcHosts and metricsScopes selectors, which resolve to explicit projects.
scope.projects []string no max items 100; items pattern ^[a-z][a-z0-9-]{4,28}[a-z0-9]$ Projects lists GCP project IDs whose GKE clusters get Cluster Agent profiles, in addition to the management project. The agent’s service account needs the read roles in each: the install’s Terraform binds them from the same scope input that renders this block, and a CR edited by hand needs the same grants made by hand; a project it cannot list is reported with the reason (denied, api-disabled, unreachable) and its existing profiles are kept.
scope.folders []string no max items 100; items pattern ^[0-9]{1,20}$ Folders lists GCP folder IDs (numeric) whose every project, at any depth, contributes its GKE clusters, resolved through Cloud Asset Inventory on each run. The agent’s service account needs roles/cloudasset.viewer and the read roles on the folder: the install’s Terraform binds them from the same scope input that renders this block, and a CR edited by hand needs the same grants made by hand; a folder it cannot read freezes its previous members and is reported with the reason.
scope.organizations []string no max items 100; items pattern ^[0-9]{1,20}$ Organizations lists GCP organisation IDs (numeric), resolved the way Folders are.
scope.sharedVpcHosts []string no max items 100; items pattern ^[a-z][a-z0-9-]{4,28}[a-z0-9]$ SharedVpcHosts lists Shared VPC host project IDs; every service project attached to the host contributes its GKE clusters, resolved on each run from the Compute API (gcloud compute shared-vpc list-associated-resources). The host project itself is not included: name it in projects if its clusters are wanted. A project that is not a Shared VPC host resolves to no members: the API answers HTTP 400 for it, which the reconcile reads as an empty membership rather than a failed lookup, so a misdeclared host cannot hold the scope prune. Nothing is inherited through a Shared VPC, so the agent’s service account needs the read roles in each resolved project, and compute.projects.get in the host project (roles/compute.viewer carries it): the install’s Terraform resolves the host at plan time and binds both (the design’s §10 step 3), so a service project attached after the last apply reads denied until the next one; a CR edited by hand needs the grants made by hand. A lookup that fails freezes the selector’s previous members and holds the scope prune, and is reported in the snapshot’s containers array under sharedVpcHosts/<host>.
scope.metricsScopes []string no max items 100; items pattern ^[a-z][a-z0-9-]{4,28}[a-z0-9]$ MetricsScopes lists Cloud Monitoring Metrics Scope scoping-project IDs; every monitored project of the scope contributes its GKE clusters, resolved on each run from the Monitoring API (gcloud beta monitoring metrics-scopes describe). The API names monitored projects by number, so each is mapped to its ID with gcloud projects describe, and a monitored project the agent cannot read is reported by number under the naming call’s outcome (denied for a 403). Nothing is inherited through a Metrics Scope, so the agent’s service account needs the read roles in each resolved project: the install’s Terraform resolves the scope at plan time and binds them (the design’s §10 step 3), so a project linked after the last apply reads denied until the next one; a CR edited by hand needs the grants made by hand. The lookup itself needs to read the scope in the scoping project: roles/monitoring.metricsScopesViewer (resourcemanager.projects.get and resourcemanager.projects.list) is the narrowest role that grants it, and the read roles the scope binds carry both between them. The Monitoring API has to be enabled in the scoping project. A lookup that fails freezes the selector’s previous members and holds the scope prune, and is reported in the snapshot’s containers array under metricsScopes/<scope>.
scope.maxProjects integer no 100 min 1; max 5000 MaxProjects caps the resolved set the reconcile lists on each run, the management project included: explicit projects, selector members and container members fill it in a fixed order, and a project past it reads over-cap (profiles kept, nothing created). The reconcile sizes its listing workers and budget from it, and the bootstrap gate its ceiling, so an estate of a few hundred projects declares the number it has rather than reading half of itself over-cap. The MaxItems=100 cap on each declared list is unchanged: a hand-written list past a hundred entries is the shape folders exist for. The install’s Terraform refuses a plan whose explicit projects and selector members exceed it (while a selector is declared or the cap is below its default), and renders the same value here from its scope input.
scope.exclude object no Exclude subtracts projects and clusters after every selector has contributed.
scope.exclude.projects []string no max items 100; items pattern ^[a-z0-9*?\[\]!-]{1,63}$ Projects are project IDs or shell-style globs (*-sandbox) matched against each resolved project ID. A match on the management project is ignored and recorded in the scope snapshot, never applied.
scope.exclude.clusters []object no max items 100 Clusters names single clusters by the full triple, because cluster names are unique only within a project and location. This replaces the RECONCILE_EXCLUDE environment variable, which matched bare names across every project. An excluded cluster gets no Cluster Agent, and fleet answers, the onboarding sweep and the fleet-audit collectors that list clusters leave it out. Still covering it: the daily upgrade-readiness watch, the networking audit’s subnet usage, the GCE audit’s instance listing, and the third-party gke MCP server’s list_clusters.
scope.exclude.clusters[].projectId string yes pattern ^[a-z0-9][a-z0-9-]*$; max length 63 ProjectID is the project the cluster lives in.
scope.exclude.clusters[].location string yes pattern ^[a-z0-9][a-z0-9-]*$; max length 63 Location is the cluster’s region or zone.
scope.exclude.clusters[].clusterName string yes pattern ^[a-z0-9][a-z0-9-]*$; max length 63 ClusterName is the GKE cluster’s name.
storage object no Storage sizes the agent’s /opt/data volume and the shell sandbox’s matching data volume.
storage.size resource.Quantity no Size is the request of the <name>-data claim and of the shell sandbox’s data claim. The operator requests the same size for both claims and grows the sandbox’s claim first, so the sandbox mirror fits. A StorageClass or a quota that refuses the growth of one claim can leave the two claims at different sizes. Absent, the operator default applies (50Gi). Raising it expands both existing claims online where the StorageClass sets allowVolumeExpansion; where it does not, the claims keep their size and the DataVolumeNotExpanded condition says so. The admission webhook refuses a lower value and a value of zero or less, because a claim cannot shrink. Removing the field never shrinks a claim.
Field Type Required Default Constraints Description
phase string no Phase is the overall state (Pending, Provisioning, Ready, Degraded, Failed).
observedGeneration integer no ObservedGeneration is the .metadata.generation the status was last computed from.
address string no Address is the fully qualified domain name (FQDN) of the agent service.
lastReconcileTime metav1.Time no LastReconcileTime is the timestamp when the operator last updated this status.
conditions []metav1.Condition no Conditions represent the latest available observations of the instance’s state.
deploymentStatus object no DeploymentStatus tracks the state of the underlying compute.
deploymentStatus.name string no Name is the exact name of the underlying Kubernetes Deployment.
deploymentStatus.readyReplicas integer no ReadyReplicas indicates how many replicas are fully ready.
serviceStatus object no ServiceStatus holds internal/external endpoints.
serviceStatus.endpoint string no Endpoint is the primary URL or IP (including protocol and port) to reach the agent.
storageStatus object no StorageStatus tracks PVC binding state and the data volume’s usage.
storageStatus.bound boolean no Bound indicates if the primary PVC has been successfully provisioned.
storageStatus.capacityBytes integer no CapacityBytes is the size of the filesystem on the agent’s data volume, as the gateway pod’s event watcher last read it. Absent when the watcher is disabled or the operator has no current reading from it.
storageStatus.freeBytes integer no FreeBytes is the space left on the agent’s data volume for the agent’s own non-root user, read with CapacityBytes. The DataVolumeNearlyFull condition is computed from the two.
telemetry object no Telemetry reports the resolved OpenTelemetry export configuration.
telemetry.otlpEndpoint string no OTLPEndpoint is the collector endpoint written into the agent pod. Empty when the source is None, which is the one case where the pod is given no endpoint at all.
telemetry.otlpEndpointSource string no OTLPEndpointSource is how the endpoint was chosen: DeploymentEnv, Spec, OperatorEnv, Discovered, Default, or None. None means discovery completed and this cluster has no collector, so the agent runs with OTEL_SDK_DISABLED=true and exports nothing; Default still means the GKE managed collector, and is what an install gets when discovery is switched off or could not complete.
networkPolicy object no NetworkPolicy reports the resolved egress NetworkPolicy configuration.
networkPolicy.generated boolean no Generated reports whether the operator is managing a NetworkPolicy for this agent: true once a reconcile has generated one, false when spec.networkPolicy.enabled is false. It is written only by the Ready status update, so read it alongside the Ready condition — a CR that went Degraded before its first successful reconcile reports false because nothing has resolved the field yet, not because generation is off.
networkPolicy.dnsClusterIPs []string no DNSClusterIPs are the ClusterIPs written into rule 1.
networkPolicy.dnsClusterIPsSource string no DNSClusterIPsSource reports which rung answered the DNS ClusterIP (Annotation, Spec, OperatorEnv, Discovered, or Default).
networkPolicy.metadataDaemonIP string no MetadataDaemonIP is the post-NAT daemon IP in the LiteLLM policy’s metadata rule, empty when suppressed.
networkPolicy.metadataDaemonPort integer no MetadataDaemonPort is the post-NAT daemon port in the LiteLLM policy’s metadata rule, resolved from the live DaemonSet when metadataDaemonIPSource is Discovered, else the documented default (988).
networkPolicy.metadataDaemonIPSource string no MetadataDaemonIPSource reports which rung answered the metadata daemon IP (Annotation, Spec, OperatorEnv, Discovered, Default, or Suppressed).
usage object no Usage summarises the agent’s activity: the interfaces its spec enables and aggregate counters of what it has done.
usage.sessionsTotal integer no SessionsTotal is the cumulative count of sessions the agent opened: the triage sessions the event watcher opened for events the daemon delivered (its k8s_event_watcher_triage_sessions_total; a session whose inject was filtered, suppressed or failed is not counted) plus the chat conversations the gateway opened a session for (the session_store plugin’s kubeagents_chat_sessions_total on a today install, the A2A gateway’s kubeagents_a2a_gateway_sessions_total on a next install). A chat conversation that resumes after the session store’s retention window (seven days by default) counts again. Read every five minutes and kept monotonic across restarts, like toolExecutionsTotal.
usage.eventsIngestedTotal integer no EventsIngestedTotal is the cumulative count of cluster events the event watcher accepted for triage: past its reason filter and its dedup window, and not turned away by the agent. Read from the watcher’s k8s_event_watcher_events_injected_total every five minutes, kept monotonic across pod, process and operator restarts, and across gateway replicas counted once rather than once per replica; it under-counts rather than over-counts when a listener cannot be read. Events the watcher merely observed are not counted.
usage.toolExecutionsTotal integer no ToolExecutionsTotal is the cumulative count of CLI and diagnostic tool invocations the credential broker ran, successful or not, plus requests it rejected or failed on before running: its success and error outcomes. Read from the broker’s kubeagents_tool_invocations_total every five minutes and kept monotonic the same way; commands refused by policy, busy and abandoned are not counted here but in ToolExecutionsBlockedTotal.
usage.remediationsProposedTotal integer no RemediationsProposedTotal is the cumulative count of proposals the credential broker’s version-control route opened: proposal-create requests the forge accepted. Read from the broker’s kubeagents_vcs_requests_total under verb proposal-create and status success, summed over its provider label, every five minutes and kept monotonic the same way as ToolExecutionsTotal; a proposal the forge refused or that failed is not counted. Moves LastActiveTime.
usage.remediationsMergedTotal integer no RemediationsMergedTotal is the forge’s count now of the proposals this install authored in its managed repositories that are merged: the broker’s readings sweep asks the forge, and this is its kubeagents_vcs_proposals under state merged, projected as the latest poll’s reading. A reading, not an accumulation: it falls when a repository leaves the managed list, and the name keeps its Total for the readers that already use it. Part of the outcomes reading with RemediationsOpen and ProposalsAuthored: one poll that cannot read the broker, or reads it without that reading, leaves the three where they were, and a second in a row clears them to absent; the broker itself stops serving a reading more than an hour old. A 0 is a reading. A merge no longer moves LastActiveTime: a reading cannot say when a merge happened, and a merge is a person’s or a forge rule’s act.
usage.remediationsOpen integer no RemediationsOpen is the forge’s count now of this install’s proposals that are open: kubeagents_vcs_proposals under state open. Part of the outcomes reading, absent on the same terms as RemediationsMergedTotal. Does not move LastActiveTime.
usage.proposalsAuthored integer no ProposalsAuthored is the forge’s count now of every proposal this install authored in its managed repositories, open, merged or closed, its own closes included: kubeagents_vcs_proposals summed over state. The denominator of the merge rate, from the same reading as RemediationsMergedTotal; RemediationsProposedTotal, the install’s own record of the proposals it opened, can differ from it. Part of the outcomes reading, absent on the same terms as RemediationsMergedTotal. Does not move LastActiveTime.
usage.chatMessagesInboundTotal integer no ChatMessagesInboundTotal is the cumulative count of chat messages that reached the gateway, from every platform it serves: Google Chat, Slack, Teams and its own API port alike. Counted at the gateway’s dispatch hook, which Hermes runs after dropping its own internal events and before it checks the sender against the allowlist, so an unlisted user’s message counts and nothing the gateway never hands to the hook does. Read every five minutes from the chat_metrics plugin’s kubeagents_chat_messages_inbound_total on the platform-agent container of the gateway pod that runs Hermes (above one replica, the leader alone), summed across the pods that have led, and, on a next-mode install, from the A2A gateway’s kubeagents_a2a_gateway_messages_inbound_total as well, since Chat ingress is that gateway’s there; kept monotonic the same way as ToolExecutionsTotal. Moves LastActiveTime, so a chat turn that runs no brokered command is activity, and LastHumanActiveTime, which nothing else moves.
usage.toolExecutionsBlockedTotal integer no ToolExecutionsBlockedTotal is the cumulative count of requests the credential broker did not run to an exit: its blocked outcome (a policy refusal), its busy outcome (the broker’s command slots or its child memory budget held the request for the whole wait) and its abandoned outcome (the caller hung up while the request was queued or running). Read from the same kubeagents_tool_invocations_total as ToolExecutionsTotal, over the other three statuses, every five minutes and kept monotonic the same way; the two together are every request the broker’s exec route took as far as its policy; a request it rejected as malformed before that reaches neither. A rising share of this against ToolExecutionsTotal says the agent keeps asking for what its mode or policy refuses, or that the broker is saturated. Moves LastActiveTime: a refused command is still the agent at work.
usage.brokerFailuresTotal integer no BrokerFailuresTotal is the cumulative count of brokered work the credential broker took on and could not carry out for a reason that is its own or its upstream’s: a command that ran past its deadline (exit 124), a broker fault on the exec route (a 500), a git failure on the version-control route (a 500 or 502), a failed forge token mint (a 502), or any 5xx on the Cloud API, MCP and watcher relays, whose upstream status passes through. A command’s own non-zero exit is not one (that is ToolExecutionsTotal’s error outcome), nor is a policy refusal or the broker’s own busy (a 403 or 503; the version-control route’s 503 for an unreadable managed-repository list is a fault and does count), nor a chat or Slack post the forge refused for the caller’s channel or scope; the broker’s own chat event pull failing, its Pub/Sub subscription or its credential to it, does count. Read from the broker’s kubeagents_broker_execution_failures_total, summed over its kind and tool labels, every five minutes and kept monotonic the same way as ToolExecutionsTotal; a timed-out command counts here and as an error there. A rising share of this against ToolExecutionsTotal says the broker, its credentials or its upstreams are failing, where a policy refusal is the tool counter’s blocked outcome in Prometheus. Does not move LastActiveTime: a watcher retrying an unreachable fleet cluster through the relay is not the agent at work.
usage.chatTurnsErroredTotal integer no ChatTurnsErroredTotal is the cumulative count of synchronous chat turns the gateway failed to complete: on the Hermes gateway, an exception out of the turn, a result the agent marked failed (a provider 429 or 5xx, a connection error, the gateway timeout) or a provider authentication failure, counted by the chat_metrics plugin’s kubeagents_chat_turns_errored_total on the gateway pod that runs Hermes; and on a next-mode install, an A2A task that ended failed or rejected, read from the A2A gateway’s kubeagents_a2a_gateway_task_terminals_total under those two states. That family carries no platform, so on next it counts every A2A task’s terminal, a door’s program-to-program task included, not chat alone; rejected is a task the executor refused, counted as the design asks. Read every five minutes and kept monotonic the same way as ToolExecutionsTotal. Does not move LastActiveTime: the message that started the turn already did.
usage.backgroundTasksGaveUpTotal integer no BackgroundTasksGaveUpTotal is the cumulative count of background cards on the agent’s board whose breaker tripped: cards that exhausted their attempts and gave up, whether a cluster event or a chat ask started them. Hermes parks such a card as blocked and records a gave_up event against it, and that event is what is counted, once per card. Read every five minutes from the chat_metrics plugin’s kubeagents_kanban_tasks_gave_up_total on the gateway pod that runs Hermes, which reads the board at scrape time and counts the cards that gave up since its listener started (the ones that had already given up are its baseline, so a restart adds nothing twice; a card that gives up while the gateway restarts is missed, an under-count); kept monotonic the same way as ToolExecutionsTotal. Does not move LastActiveTime: a card giving up is not the agent at work.
usage.negativeFeedbackTotal integer no NegativeFeedbackTotal is the cumulative count of thumbs-down reactions put on one of the bot’s Slack messages: the chat_feedback_reactions hook on the Hermes gateway counts a -1 or thumbsdown reaction, skin tone or not, whose reactor is not the bot and whose message is the bot’s, once per reactor and message within one gateway process (a thumbs-down taken off and put back is one; after a restart the same one counts again, and a reaction before the first inbound message after a start is not counted), into the chat_metrics plugin’s kubeagents_chat_feedback_negative_total, which is read every five minutes and kept monotonic the same way as ToolExecutionsTotal. Slack through the Hermes gateway only: Google Chat delivers no reaction events, and the A2A gateway’s Slack adapter does not subscribe to them, so on a next-mode install with Slack through A2A this stays at 0. Does not move LastActiveTime: the message the person reacted to already did.
usage.remediationsClosedUnmergedTotal integer no RemediationsClosedUnmergedTotal is the forge’s count now of this install’s proposals closed without a merge by a person or a bot: kubeagents_vcs_proposals_closed_by under actor human plus actor bot. A close the install made itself, the fleet audit tidying a stale proposal, is not counted. A reading, not an accumulation, projected as the latest poll’s; the name keeps its Total for the readers that already use it. A high share of this against RemediationsMergedTotal says the agent’s proposals are being rejected. Part of the details reading with ProposalHumanTouches and ProposalsMergedUntouched: one poll that cannot read the broker, or reads it without that reading, leaves the three where they were, and a second in a row clears them to absent; a broker still warming serves the outcomes reading without this one, and one whose last sweep could not read the details keeps serving the details it last read, for an hour. A 0 is a reading. Does not move LastActiveTime: a close is not the agent at work.
usage.proposalHumanTouches integer no ProposalHumanTouches is the forge’s count now of the events people made on this install’s proposals: reviews, inline and conversation comments, commits on the source branch and /agent addresses by any account the forge does not mark as automation, summed over every proposal (kubeagents_vcs_proposal_touches under actor human, summed over kind). Part of the details reading, absent on the same terms as RemediationsClosedUnmergedTotal. Moves neither LastActiveTime nor LastHumanActiveTime.
usage.proposalsMergedUntouched integer no ProposalsMergedUntouched is the forge’s count now of this install’s merged proposals that carried no human touch of any kind: kubeagents_vcs_proposal_touches_per_proposal under outcome merged and le 0. Part of the details reading, absent on the same terms as RemediationsClosedUnmergedTotal. Does not move LastActiveTime.
usage.clustersRegistered integer no ClustersRegistered is the number of clusters the event watcher has built a client for: the management cluster and the Cluster Agent profiles the GKE API would describe, the management cluster counted once even where a profile also covers it. Read every five minutes as the number of k8s_event_watcher_cluster_up series the watcher exports, the largest reading across gateway replicas. The watcher re-reads its profiles directory every minute, so a cluster that joins is counted within about two minutes and one whose profile is removed drops out on the next read; a restart rebuilds the count from the directory as it stands. A gauge, not a counter: it falls on a removal or after such a restart. Absent when the operator has no current reading: before the first poll, while the event watcher is disabled, and after two polls in a row in which no gateway replica could be read, whether its listener failed, its pod was not running, or the operator could not reach it; the CR’s events and the gateway pod’s state say which. A 0 is a reading: the watcher answered and exports no series.
usage.clustersMonitored integer no ClustersMonitored is how many of ClustersRegistered are delivering events: the k8s_event_watcher_cluster_up series at 1, whose informer has completed its initial list. The difference from ClustersRegistered is the number of clusters silently unwatched, a stuck informer, a 403 on the events list, or a stopped one. A 0 is a reading, and the normal one while the watcher starts, so read a fall alongside the pod’s age. Absent on the same terms as ClustersRegistered: one poll in which no gateway replica could be read leaves both where they were, and a second in a row clears both, because the operator then has no current reading and an absent field says so.
usage.activeInterfaces []string no ActiveInterfaces lists the communication channels the spec enables, sorted: “dashboard” unless spec.harness.hermes.dashboardEnabled is false, and “googlechat”, “slack” and “teams” for each spec.integration entry whose enabled is true. Resolved on every reconcile and written by the Ready status update when it changes; a pass that parks the CR Degraded leaves the previous value, so read it alongside the Ready condition, as networkPolicy.generated is read. Absent while the served CRD predates it.
usage.lastActiveTime metav1.Time no LastActiveTime is the time of the last poll in which a counter above moved: a brokered command ran or was refused, a proposal was opened, a chat message reached the gateway, or an event was accepted for triage; BrokerFailuresTotal, ChatTurnsErroredTotal, BackgroundTasksGaveUpTotal and NegativeFeedbackTotal alone leave it where it was, and the six proposal readings never move it. Scheduled maintenance jobs that run brokered commands move it too – the Controller Stall Watch cron runs some every 30 minutes by default – so it marks agent activity of any origin, not human or operator use alone; LastHumanActiveTime is the one that does. Advances at most once per five minutes.
usage.lastHumanActiveTime metav1.Time no LastHumanActiveTime is the time of the last poll in which the one counter only a person moves advanced: ChatMessagesInboundTotal, a chat message that reached the gateway, counted before the sender allowlist, so an unlisted sender is still a person. Nothing else moves it. Brokered commands and accepted events come from the stall-watch cron and the event watcher on their own, and proposals from the fleet audits, which open them with nobody asking; those move LastActiveTime and leave this alone, as the proposal readings leave both alone, so an install nobody has talked to in a month shows it. Absent until a person has sent a message since the operator that writes it started. Advances at most once per five minutes.