|
| 1 | +# OpenClaw on Agent Substrate |
| 2 | + |
| 3 | +## 1. Install Substrate on your Kind cluster |
| 4 | + |
| 5 | +You can clone the kagent fork of substrate [here](https://github.com/kagent-dev/substrate). |
| 6 | + |
| 7 | +These instructions use a Kind cluster called `kind` (`KIND_CLUSTER_NAME=kind`). |
| 8 | + |
| 9 | +```bash |
| 10 | +cd substrate |
| 11 | + |
| 12 | +./hack/create-kind-cluster.sh |
| 13 | +./hack/install-ate-kind.sh --deploy-ate-system |
| 14 | +``` |
| 15 | + |
| 16 | +`--deploy-ate-system` installs the **control plane only** (ate-api, ate-controller, atelet, atenet, …). Your registry catalog will show `ateapi-*`, `atelet-*`, etc., but **not** ateom until you build it. |
| 17 | + |
| 18 | +Build and push **ateom-gvisor** (required for the WorkerPool `ateomImage`): |
| 19 | + |
| 20 | +```bash |
| 21 | +# build the ateom-gvisor image from the substrate repo root |
| 22 | +export KO_DOCKER_REPO=localhost:5001 |
| 23 | +export KO_DEFAULTPLATFORMS=linux/$(go env GOARCH) |
| 24 | +./hack/run-tool.sh ko build -B ./cmd/ateom-gvisor |
| 25 | +``` |
| 26 | + |
| 27 | +## kagent AgentHarness with substrate runtime |
| 28 | + |
| 29 | +kagent generates a per-harness `ActorTemplate` and uses an existing `WorkerPool`. |
| 30 | + |
| 31 | +Install kagent (Substrate must already be running in the cluster): |
| 32 | + |
| 33 | +```bash |
| 34 | +export KIND_CLUSTER_NAME=kind |
| 35 | +make helm-install KAGENT_HELM_EXTRA_ARGS="\ |
| 36 | + --set controller.substrate.enabled=true \ |
| 37 | + --set controller.substrate.ateApiEndpoint=dns:///api.ate-system.svc:443 \ |
| 38 | + --set controller.substrate.ateApiInsecure=true \ |
| 39 | + --set substrateWorkerPool.create=true \ |
| 40 | + --set substrateWorkerPool.ateomImage=localhost:5001/ateom-gvisor:latest" |
| 41 | +``` |
| 42 | + |
| 43 | +The generated `ActorTemplate` uses `controller.substrate.pauseImage`, `controller.substrate.runscAMD64URL`, `controller.substrate.runscAMD64SHA256`, `controller.substrate.runscARM64URL`, and `controller.substrate.runscARM64SHA256` from the Helm values Override them with `--set` or a values file when you need to pin a different gVisor build. |
| 44 | + |
| 45 | +Create a harness. If `snapshotsConfig` is omitted, kagent defaults it to `gs://ate-snapshots/<namespace>/<agentharnessname>`. |
| 46 | + |
| 47 | +- **Worker pool** — reference an existing pool (`workerPoolRef`) or configure a controller default WorkerPool |
| 48 | +- **Gateway token** — required per harness with either `gatewayToken` or `gatewayTokenSecretRef` |
| 49 | + |
| 50 | +```yaml |
| 51 | +apiVersion: kagent.dev/v1alpha2 |
| 52 | +kind: AgentHarness |
| 53 | +metadata: |
| 54 | + name: peterj-claw |
| 55 | + namespace: kagent |
| 56 | +spec: |
| 57 | + runtime: substrate |
| 58 | + backend: openclaw |
| 59 | + description: OpenClaw on Agent Substrate |
| 60 | + modelConfigRef: default-model-config |
| 61 | + substrate: |
| 62 | + # Optional: defaults to gs://ate-snapshots/kagent/peterj-claw |
| 63 | + # snapshotsConfig: |
| 64 | + # location: gs://ate-snapshots/kagent/peterj-claw |
| 65 | + |
| 66 | + # Required unless the controller has a default WorkerPool configured. |
| 67 | + workerPoolRef: |
| 68 | + name: kagent-default |
| 69 | + |
| 70 | + # Required: configure the OpenClaw gateway token for this harness. |
| 71 | + # Use either gatewayToken or gatewayTokenSecretRef. The Secret must contain key "token". |
| 72 | + gatewayToken: test-token |
| 73 | + |
| 74 | + # gatewayTokenSecretRef: |
| 75 | + # name: openclaw-gateway-token |
| 76 | + |
| 77 | + # Optional: override the sandbox image used in the ActorTemplate (must be digest-pinned). |
| 78 | + # workloadImage: ghcr.io/kagent-dev/nemoclaw/sandbox-base@sha256:d52bee415dc4c0dba7164f9eabe727574c056d4f211781f20af249707883a3b4 |
| 79 | +``` |
| 80 | + |
| 81 | +kagent creates an `ActorTemplate` that looks roughly like this: |
| 82 | + |
| 83 | +```yaml |
| 84 | +apiVersion: ate.dev/v1alpha1 |
| 85 | +kind: ActorTemplate |
| 86 | +metadata: |
| 87 | + name: peterj-claw |
| 88 | + namespace: kagent |
| 89 | + labels: |
| 90 | + app.kubernetes.io/managed-by: kagent |
| 91 | + kagent.dev/agent-harness: peterj-claw |
| 92 | +spec: |
| 93 | + pauseImage: gcr.io/gke-release/pause@sha256:bcbd57ba5653580ec647b16d8163cdd1112df3609129b01f912a8032e48265da |
| 94 | + runsc: |
| 95 | + amd64: |
| 96 | + url: gs://gvisor/releases/nightly/2026-05-19/x86_64/runsc |
| 97 | + sha256Hash: a397be1abc2420d26bce6c70e6e2ff96c73aaaab929756c56f5e2089ea842b63 |
| 98 | + arm64: |
| 99 | + url: gs://gvisor/releases/nightly/2026-05-19/aarch64/runsc |
| 100 | + sha256Hash: 1ba2366ae2efceba166046f51a4104f9261c9cb72c6db8f5b3fe2dc57dea86b9 |
| 101 | + workerPoolRef: |
| 102 | + name: peterj-claw-wp |
| 103 | + namespace: kagent |
| 104 | + snapshotsConfig: |
| 105 | + location: gs://ate-snapshots/kagent/peterj-claw |
| 106 | + containers: |
| 107 | + - name: openclaw |
| 108 | + image: ghcr.io/kagent-dev/nemoclaw/sandbox-base@sha256:d52bee415dc4c0dba7164f9eabe727574c056d4f211781f20af249707883a3b4 |
| 109 | + ports: |
| 110 | + - containerPort: 80 |
| 111 | + command: |
| 112 | + - /bin/sh |
| 113 | + - -c |
| 114 | + - | |
| 115 | + # Generated by kagent: |
| 116 | + # 1. writes ~/.openclaw/openclaw.json from modelConfigRef/channels/gateway token |
| 117 | + # 2. configures gateway.controlUi.basePath for the kagent proxy path |
| 118 | + # 3. starts `openclaw gateway run --port 80 --allow-unconfigured` |
| 119 | + # 4. waits for the gateway and tails the log |
| 120 | + env: |
| 121 | + - name: HOME |
| 122 | + value: /root |
| 123 | +``` |
| 124 | +
|
| 125 | +The generated `command` contains a base64-encoded `openclaw.json`, so the live object will be more verbose than the abbreviated example above. `pauseImage`, runsc URLs and hashes, and the default workload image come from controller/Helm configuration unless overridden on the `AgentHarness`; the gateway token comes from `spec.substrate.gatewayToken` or `gatewayTokenSecretRef`. kagent also sets `gateway.controlUi.basePath` to `/api/agentharnesses/<namespace>/<name>/gateway` so OpenClaw serves the Control UI under the same path kagent proxies. |
| 126 | + |
| 127 | +When `modelConfigRef` or `spec.channels` are set, credentials are **not** copied into the ActorTemplate or `openclaw.json` as plaintext. kagent writes `valueFrom.secretKeyRef` (or inline `value` for harness inline tokens) on the ActorTemplate container env; Substrate `ate-api` resolves those refs at actor resume. In `openclaw.json`, kagent uses OpenClaw [env SecretRefs](https://docs.openclaw.ai/gateway/secrets) (`{source:"env",provider:"default",id:"<VAR>"}`) for `models.providers.*.apiKey`, `channels.telegram.accounts.*.botToken`, and `channels.slack.accounts.*.botToken` / `appToken`. Rotate a Secret and recreate the ActorTemplate golden snapshot when keys change. |
| 128 | + |
| 129 | +With `controller.substrate.enabled=true`, the kagent Helm chart installs a namespace-scoped Role and RoleBinding so `ate-api-server` (in `ate-system` by default) can `get` Secrets and ConfigMaps referenced by generated ActorTemplates. Harnesses in other namespaces need that namespace listed in `rbac.namespaces` (or a matching RoleBinding applied manually). |
| 130 | + |
| 131 | +Port-forward the UI: |
| 132 | + |
| 133 | +```bash |
| 134 | +kubectl port-forward -n kagent svc/kagent-ui 8001:8080 |
| 135 | +``` |
| 136 | + |
| 137 | +Navigate to the deployed agent harness. If the OpenClaw Control UI asks for a gateway connection, use: |
| 138 | + |
| 139 | +- Gateway URL: `http://localhost:8001/api/agentharnesses/kagent/peterj-claw/gateway/` |
| 140 | +- Gateway token: `test-token` |
| 141 | + |
| 142 | +The gateway URL must include the trailing slash. The token is the value configured in `spec.substrate.gatewayToken`, or the Secret value referenced by `spec.substrate.gatewayTokenSecretRef`; enter it in the token/credentials field rather than relying on a `token` query parameter. |
| 143 | + |
| 144 | +kagent proxies UI traffic to the actor OpenClaw gateway through Substrate's **atenet-router** (Envoy) using the actor `Host` header (`<actor-id>.actors.resources.substrate.ate.dev`). The default router URL is `http://atenet-router.ate-system.svc:80`; override with `controller.substrate.atenetRouterURL` when needed. |
0 commit comments