{
  "description": "AgentHarness is a generic remote execution environment provisioned by a\nbackend (OpenClaw or Hermes) running on Agent Substrate.",
  "properties": {
    "apiVersion": {
      "description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
      "type": "string"
    },
    "kind": {
      "description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
      "type": "string"
    },
    "metadata": {
      "type": "object"
    },
    "spec": {
      "description": "AgentHarnessSpec describes a generic remote execution environment that agents\n(or human operators) can attach to via exec or SSH.\n\nAn AgentHarness is distinct from a SandboxAgent: it has no agent runtime baked\nin. The backend is responsible for provisioning an environment that stays\nready to accept incoming commands.",
      "properties": {
        "backend": {
          "description": "Backend selects the control plane to use. Required.",
          "enum": [
            "openclaw",
            "hermes"
          ],
          "type": "string"
        },
        "channels": {
          "description": "Channels configures Telegram and Slack integrations for OpenClaw inside the harness VM.",
          "items": {
            "description": "AgentHarnessChannel declares one messenger binding inside a harness VM.",
            "properties": {
              "name": {
                "description": "Name is a stable id for this binding (OpenClaw channels.*.accounts key).",
                "minLength": 1,
                "type": "string"
              },
              "slack": {
                "description": "Slack configures Slack when type is Slack.",
                "properties": {
                  "appToken": {
                    "description": "AgentHarnessChannelCredential supplies a token from an inline value or a Secret/ConfigMap key.",
                    "properties": {
                      "value": {
                        "maxLength": 8192,
                        "type": "string"
                      },
                      "valueFrom": {
                        "description": "ValueSource defines a source for configuration values from a Secret or ConfigMap",
                        "properties": {
                          "key": {
                            "description": "The key of the ConfigMap or Secret.",
                            "maxLength": 253,
                            "type": "string"
                          },
                          "name": {
                            "description": "The name of the ConfigMap or Secret.",
                            "maxLength": 253,
                            "type": "string"
                          },
                          "type": {
                            "enum": [
                              "ConfigMap",
                              "Secret"
                            ],
                            "type": "string"
                          }
                        },
                        "required": [
                          "key",
                          "name",
                          "type"
                        ],
                        "type": "object",
                        "additionalProperties": false
                      }
                    },
                    "type": "object",
                    "x-kubernetes-validations": [
                      {
                        "message": "Exactly one of value or valueFrom must be specified",
                        "rule": "(has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom))"
                      }
                    ],
                    "additionalProperties": false
                  },
                  "botToken": {
                    "description": "AgentHarnessChannelCredential supplies a token from an inline value or a Secret/ConfigMap key.",
                    "properties": {
                      "value": {
                        "maxLength": 8192,
                        "type": "string"
                      },
                      "valueFrom": {
                        "description": "ValueSource defines a source for configuration values from a Secret or ConfigMap",
                        "properties": {
                          "key": {
                            "description": "The key of the ConfigMap or Secret.",
                            "maxLength": 253,
                            "type": "string"
                          },
                          "name": {
                            "description": "The name of the ConfigMap or Secret.",
                            "maxLength": 253,
                            "type": "string"
                          },
                          "type": {
                            "enum": [
                              "ConfigMap",
                              "Secret"
                            ],
                            "type": "string"
                          }
                        },
                        "required": [
                          "key",
                          "name",
                          "type"
                        ],
                        "type": "object",
                        "additionalProperties": false
                      }
                    },
                    "type": "object",
                    "x-kubernetes-validations": [
                      {
                        "message": "Exactly one of value or valueFrom must be specified",
                        "rule": "(has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom))"
                      }
                    ],
                    "additionalProperties": false
                  },
                  "hermes": {
                    "description": "Hermes configures Hermes-specific Slack settings.",
                    "properties": {
                      "allowedUserIDs": {
                        "description": "AllowedUserIDs restricts which Slack member IDs may interact with the bot (SLACK_ALLOWED_USERS).",
                        "items": {
                          "type": "string"
                        },
                        "maxItems": 1024,
                        "type": "array"
                      },
                      "allowedUserIDsFrom": {
                        "description": "ValueSource defines a source for configuration values from a Secret or ConfigMap",
                        "properties": {
                          "key": {
                            "description": "The key of the ConfigMap or Secret.",
                            "maxLength": 253,
                            "type": "string"
                          },
                          "name": {
                            "description": "The name of the ConfigMap or Secret.",
                            "maxLength": 253,
                            "type": "string"
                          },
                          "type": {
                            "enum": [
                              "ConfigMap",
                              "Secret"
                            ],
                            "type": "string"
                          }
                        },
                        "required": [
                          "key",
                          "name",
                          "type"
                        ],
                        "type": "object",
                        "additionalProperties": false
                      },
                      "homeChannel": {
                        "description": "HomeChannel is the default Slack channel ID for cron/scheduled messages (SLACK_HOME_CHANNEL).",
                        "type": "string"
                      },
                      "homeChannelName": {
                        "description": "HomeChannelName is a human-readable label for HomeChannel (SLACK_HOME_CHANNEL_NAME).",
                        "type": "string"
                      }
                    },
                    "type": "object",
                    "x-kubernetes-validations": [
                      {
                        "message": "allowedUserIDs and allowedUserIDsFrom are mutually exclusive",
                        "rule": "!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))"
                      }
                    ],
                    "additionalProperties": false
                  },
                  "openclaw": {
                    "description": "OpenClaw configures OpenClaw-specific Slack routing.",
                    "properties": {
                      "allowlistChannels": {
                        "description": "AllowlistChannels is required when channelAccess is allowlist.",
                        "items": {
                          "type": "string"
                        },
                        "maxItems": 1024,
                        "type": "array"
                      },
                      "channelAccess": {
                        "description": "AgentHarnessChannelAccess controls whether the bot listens broadly or only on an allowlist.",
                        "enum": [
                          "allowlist",
                          "open",
                          "disabled"
                        ],
                        "type": "string"
                      },
                      "interactiveReplies": {
                        "default": true,
                        "type": "boolean"
                      }
                    },
                    "type": "object",
                    "x-kubernetes-validations": [
                      {
                        "message": "allowlistChannels is required when channelAccess is allowlist",
                        "rule": "!has(self.channelAccess) || self.channelAccess != 'allowlist' || (has(self.allowlistChannels) && size(self.allowlistChannels) > 0)"
                      }
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "appToken",
                  "botToken"
                ],
                "type": "object",
                "additionalProperties": false
              },
              "telegram": {
                "description": "AgentHarnessTelegramChannelSpec configures Telegram when AgentHarnessChannel.type is Telegram.",
                "properties": {
                  "allowedUserIDs": {
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 1024,
                    "type": "array"
                  },
                  "allowedUserIDsFrom": {
                    "description": "ValueSource defines a source for configuration values from a Secret or ConfigMap",
                    "properties": {
                      "key": {
                        "description": "The key of the ConfigMap or Secret.",
                        "maxLength": 253,
                        "type": "string"
                      },
                      "name": {
                        "description": "The name of the ConfigMap or Secret.",
                        "maxLength": 253,
                        "type": "string"
                      },
                      "type": {
                        "enum": [
                          "ConfigMap",
                          "Secret"
                        ],
                        "type": "string"
                      }
                    },
                    "required": [
                      "key",
                      "name",
                      "type"
                    ],
                    "type": "object",
                    "additionalProperties": false
                  },
                  "botToken": {
                    "description": "AgentHarnessChannelCredential supplies a token from an inline value or a Secret/ConfigMap key.",
                    "properties": {
                      "value": {
                        "maxLength": 8192,
                        "type": "string"
                      },
                      "valueFrom": {
                        "description": "ValueSource defines a source for configuration values from a Secret or ConfigMap",
                        "properties": {
                          "key": {
                            "description": "The key of the ConfigMap or Secret.",
                            "maxLength": 253,
                            "type": "string"
                          },
                          "name": {
                            "description": "The name of the ConfigMap or Secret.",
                            "maxLength": 253,
                            "type": "string"
                          },
                          "type": {
                            "enum": [
                              "ConfigMap",
                              "Secret"
                            ],
                            "type": "string"
                          }
                        },
                        "required": [
                          "key",
                          "name",
                          "type"
                        ],
                        "type": "object",
                        "additionalProperties": false
                      }
                    },
                    "type": "object",
                    "x-kubernetes-validations": [
                      {
                        "message": "Exactly one of value or valueFrom must be specified",
                        "rule": "(has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom))"
                      }
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "botToken"
                ],
                "type": "object",
                "x-kubernetes-validations": [
                  {
                    "message": "allowedUserIDs and allowedUserIDsFrom are mutually exclusive",
                    "rule": "!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))"
                  }
                ],
                "additionalProperties": false
              },
              "type": {
                "description": "AgentHarnessChannelType selects a messenger integration for OpenClaw harness VMs.",
                "enum": [
                  "telegram",
                  "slack"
                ],
                "type": "string"
              }
            },
            "required": [
              "name",
              "type"
            ],
            "type": "object",
            "x-kubernetes-validations": [
              {
                "message": "exactly one of telegram or slack must be set and must match type",
                "rule": "(self.type == 'telegram' && has(self.telegram) && !has(self.slack)) || (self.type == 'slack' && has(self.slack) && !has(self.telegram))"
              }
            ],
            "additionalProperties": false
          },
          "maxItems": 1024,
          "type": "array"
        },
        "description": {
          "description": "Description is a short human-readable summary shown in the UI (e.g. agents list).",
          "type": "string"
        },
        "env": {
          "description": "Env is a list of environment variables injected into the harness workload.\nValues use the Kubernetes EnvVar shape; ValueFrom references are\nresolved server-side where supported.",
          "items": {
            "description": "EnvVar represents an environment variable present in a Container.",
            "properties": {
              "name": {
                "description": "Name of the environment variable.\nMay consist of any printable ASCII characters except '='.",
                "type": "string"
              },
              "value": {
                "description": "Variable references $(VAR_NAME) are expanded\nusing the previously defined environment variables in the container and\nany service environment variables. If a variable cannot be resolved,\nthe reference in the input string will be unchanged. Double $$ are reduced\nto a single $, which allows for escaping the $(VAR_NAME) syntax: i.e.\n\"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\".\nEscaped references will never be expanded, regardless of whether the variable\nexists or not.\nDefaults to \"\".",
                "type": "string"
              },
              "valueFrom": {
                "description": "Source for the environment variable's value. Cannot be used if value is not empty.",
                "properties": {
                  "configMapKeyRef": {
                    "description": "Selects a key of a ConfigMap.",
                    "properties": {
                      "key": {
                        "description": "The key to select.",
                        "type": "string"
                      },
                      "name": {
                        "default": "",
                        "description": "Name of the referent.\nThis field is effectively required, but due to backwards compatibility is\nallowed to be empty. Instances of this type with an empty value here are\nalmost certainly wrong.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names",
                        "type": "string"
                      },
                      "optional": {
                        "description": "Specify whether the ConfigMap or its key must be defined",
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "key"
                    ],
                    "type": "object",
                    "x-kubernetes-map-type": "atomic",
                    "additionalProperties": false
                  },
                  "fieldRef": {
                    "description": "Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`,\nspec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.",
                    "properties": {
                      "apiVersion": {
                        "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".",
                        "type": "string"
                      },
                      "fieldPath": {
                        "description": "Path of the field to select in the specified API version.",
                        "type": "string"
                      }
                    },
                    "required": [
                      "fieldPath"
                    ],
                    "type": "object",
                    "x-kubernetes-map-type": "atomic",
                    "additionalProperties": false
                  },
                  "fileKeyRef": {
                    "description": "FileKeyRef selects a key of the env file.\nRequires the EnvFiles feature gate to be enabled.",
                    "properties": {
                      "key": {
                        "description": "The key within the env file. An invalid key will prevent the pod from starting.\nThe keys defined within a source may consist of any printable ASCII characters except '='.\nDuring Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters.",
                        "type": "string"
                      },
                      "optional": {
                        "default": false,
                        "description": "Specify whether the file or its key must be defined. If the file or key\ndoes not exist, then the env var is not published.\nIf optional is set to true and the specified key does not exist,\nthe environment variable will not be set in the Pod's containers.\n\nIf optional is set to false and the specified key does not exist,\nan error will be returned during Pod creation.",
                        "type": "boolean"
                      },
                      "path": {
                        "description": "The path within the volume from which to select the file.\nMust be relative and may not contain the '..' path or start with '..'.",
                        "type": "string"
                      },
                      "volumeName": {
                        "description": "The name of the volume mount containing the env file.",
                        "type": "string"
                      }
                    },
                    "required": [
                      "key",
                      "path",
                      "volumeName"
                    ],
                    "type": "object",
                    "x-kubernetes-map-type": "atomic",
                    "additionalProperties": false
                  },
                  "resourceFieldRef": {
                    "description": "Selects a resource of the container: only resources limits and requests\n(limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.",
                    "properties": {
                      "containerName": {
                        "description": "Container name: required for volumes, optional for env vars",
                        "type": "string"
                      },
                      "divisor": {
                        "anyOf": [
                          {
                            "type": "integer"
                          },
                          {
                            "type": "string"
                          }
                        ],
                        "description": "Specifies the output format of the exposed resources, defaults to \"1\"",
                        "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
                        "x-kubernetes-int-or-string": true
                      },
                      "resource": {
                        "description": "Required: resource to select",
                        "type": "string"
                      }
                    },
                    "required": [
                      "resource"
                    ],
                    "type": "object",
                    "x-kubernetes-map-type": "atomic",
                    "additionalProperties": false
                  },
                  "secretKeyRef": {
                    "description": "Selects a key of a secret in the pod's namespace",
                    "properties": {
                      "key": {
                        "description": "The key of the secret to select from.  Must be a valid secret key.",
                        "type": "string"
                      },
                      "name": {
                        "default": "",
                        "description": "Name of the referent.\nThis field is effectively required, but due to backwards compatibility is\nallowed to be empty. Instances of this type with an empty value here are\nalmost certainly wrong.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names",
                        "type": "string"
                      },
                      "optional": {
                        "description": "Specify whether the Secret or its key must be defined",
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "key"
                    ],
                    "type": "object",
                    "x-kubernetes-map-type": "atomic",
                    "additionalProperties": false
                  }
                },
                "type": "object",
                "additionalProperties": false
              }
            },
            "required": [
              "name"
            ],
            "type": "object",
            "additionalProperties": false
          },
          "type": "array"
        },
        "image": {
          "description": "Image is the container image to run in the harness VM, if the backend\nsupports per-resource images. Backend openclaw pins the image\nto the OpenClaw sandbox base when this field is empty; backend hermes pins\nto the Hermes sandbox base image when empty.",
          "type": "string"
        },
        "modelConfigRef": {
          "description": "ModelConfigRef is the reference to the ModelConfig used to configure the harness.\nThe controller registers the gateway provider and, after the harness is Ready,\nwrites OpenClaw config inside the VM (~/.openclaw/openclaw.json) and starts the gateway.",
          "type": "string"
        },
        "substrate": {
          "description": "Substrate configures the Agent Substrate provisioning stack. Required.",
          "properties": {
            "snapshotsConfig": {
              "description": "SnapshotsConfig configures actor memory snapshots. Defaults to\ngs://ate-snapshots/<namespace>/<agentharnessname> when unset.",
              "properties": {
                "location": {
                  "description": "Location is the GCS URI prefix for golden and incremental snapshots.\nExample: gs://ate-snapshots/kagent/my-namespace/my-harness/",
                  "pattern": "^gs://",
                  "type": "string"
                }
              },
              "required": [
                "location"
              ],
              "type": "object",
              "additionalProperties": false
            },
            "workerPoolRef": {
              "description": "WorkerPoolRef references an existing ate.dev WorkerPool in the harness namespace.\nWhen unset, the controller uses its configured default WorkerPool.",
              "properties": {
                "apiGroup": {
                  "type": "string"
                },
                "kind": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              },
              "required": [
                "name"
              ],
              "type": "object",
              "additionalProperties": false
            },
            "workloadImage": {
              "description": "WorkloadImage overrides the default openclaw sandbox image in the ActorTemplate.",
              "type": "string"
            }
          },
          "type": "object",
          "additionalProperties": false
        }
      },
      "required": [
        "backend",
        "substrate"
      ],
      "type": "object",
      "x-kubernetes-validations": [
        {
          "message": "slack backend-specific settings must match spec.backend",
          "rule": "!has(self.channels) || self.channels.all(c, c.type != 'slack' || (has(c.slack) && ((self.backend == 'hermes' && has(c.slack.hermes) && !has(c.slack.openclaw)) || (self.backend == 'openclaw' && has(c.slack.openclaw) && !has(c.slack.hermes)))))"
        },
        {
          "message": "spec.substrate is required",
          "rule": "has(self.substrate)"
        }
      ],
      "additionalProperties": false
    },
    "status": {
      "description": "AgentHarnessStatus is the observed state of an AgentHarness.",
      "properties": {
        "backendRef": {
          "description": "BackendRef points at the harness instance on the backend control\nplane, once Ensure has succeeded at least once.",
          "properties": {
            "backend": {
              "description": "AgentHarnessBackendType selects which sandbox control plane provisions the\nenvironment. Additional backends may be added in the future.",
              "enum": [
                "openclaw",
                "hermes"
              ],
              "type": "string"
            },
            "id": {
              "type": "string"
            }
          },
          "required": [
            "backend",
            "id"
          ],
          "type": "object",
          "additionalProperties": false
        },
        "conditions": {
          "items": {
            "description": "Condition contains details for one aspect of the current state of this API Resource.",
            "properties": {
              "lastTransitionTime": {
                "description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.",
                "format": "date-time",
                "type": "string"
              },
              "message": {
                "description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.",
                "maxLength": 32768,
                "type": "string"
              },
              "observedGeneration": {
                "description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.",
                "format": "int64",
                "minimum": 0,
                "type": "integer"
              },
              "reason": {
                "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.",
                "maxLength": 1024,
                "minLength": 1,
                "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$",
                "type": "string"
              },
              "status": {
                "description": "status of the condition, one of True, False, Unknown.",
                "enum": [
                  "True",
                  "False",
                  "Unknown"
                ],
                "type": "string"
              },
              "type": {
                "description": "type of condition in CamelCase or in foo.example.com/CamelCase.",
                "maxLength": 316,
                "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$",
                "type": "string"
              }
            },
            "required": [
              "lastTransitionTime",
              "message",
              "reason",
              "status",
              "type"
            ],
            "type": "object",
            "additionalProperties": false
          },
          "type": "array"
        },
        "connection": {
          "description": "Connection is populated by the controller when the harness is ready.",
          "properties": {
            "endpoint": {
              "description": "Endpoint is the backend-specific address (gRPC target, SSH host:port,\n...) clients should use to reach the harness.",
              "type": "string"
            }
          },
          "type": "object",
          "additionalProperties": false
        },
        "observedGeneration": {
          "format": "int64",
          "type": "integer"
        }
      },
      "type": "object",
      "additionalProperties": false
    }
  },
  "type": "object"
}
