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. |