Skip to content

Commit ffc7635

Browse files
committed
feat(substrate): bump agent substrate to 0.0.8
Adapts kagent for substrate v0.0.8's atespace-scoped ActorRef identity model (rename of ActorId→ActorRef{Atespace,Name} on all actor RPCs). Maps atespace 1:1 to the SandboxAgent/AgentHarness Kubernetes namespace, adds an EnsureAtespace idempotent helper, and updates the atenet-router Host header shape to include the atespace label. Also fixes a pre-existing kagent bug that PR #2109's ActorTemplate spec immutability change surfaced: SnapshotsConfig.{OnPause,OnCommit} were left zero-value in kagent's desired spec but the API server defaults them to "Full" on admission, causing apiequality.Semantic.DeepEqual to report drift every reconcile and hot-loop delete/recreate the ActorTemplate CR. Verified end-to-end on colima+kind with substrate v0.0.8 published charts: SandboxAgent (declarative Go) and AgentHarness (openclaw) both reach Ready=True and chat round-trip works.
1 parent 6a241b7 commit ffc7635

30 files changed

Lines changed: 234 additions & 105 deletions

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ KMCP_VERSION ?= $(shell $(AWK) '/github\.com\/kagent-dev\/kmcp/ { print substr($
150150
# Substrate
151151
SUBSTRATE_ENABLED ?= false
152152
SUBSTRATE_VERSION ?= $(shell $(AWK) '/github\.com\/kagent-dev\/substrate/ { print substr($$5, 2) }' go/go.mod) # Substrate version defaults to the replace target in go.mod
153+
SUBSTRATE_REPO ?= oci://ghcr.io/kagent-dev/substrate/helm # Override for local dev when consuming a locally-published chart, e.g. oci://localhost:5001/kagent-dev/substrate/helm
153154

154155
HELM_ACTION=upgrade --install
155156

@@ -453,8 +454,8 @@ helm-tools: ## Package all tool Helm charts into the dist folder
453454
.PHONY: helm-version
454455
helm-version: ## Stamp chart versions, update dependencies, and package kagent + kagent-crds
455456
helm-version: helm-cleanup helm-agents helm-tools
456-
VERSION=$(VERSION) KMCP_VERSION=$(KMCP_VERSION) SUBSTRATE_VERSION=$(SUBSTRATE_VERSION) envsubst < helm/kagent-crds/Chart-template.yaml > helm/kagent-crds/Chart.yaml
457-
VERSION=$(VERSION) KMCP_VERSION=$(KMCP_VERSION) SUBSTRATE_VERSION=$(SUBSTRATE_VERSION) envsubst < helm/kagent/Chart-template.yaml > helm/kagent/Chart.yaml
457+
VERSION=$(VERSION) KMCP_VERSION=$(KMCP_VERSION) SUBSTRATE_VERSION=$(SUBSTRATE_VERSION) SUBSTRATE_REPO=$(SUBSTRATE_REPO) envsubst < helm/kagent-crds/Chart-template.yaml > helm/kagent-crds/Chart.yaml
458+
VERSION=$(VERSION) KMCP_VERSION=$(KMCP_VERSION) SUBSTRATE_VERSION=$(SUBSTRATE_VERSION) SUBSTRATE_REPO=$(SUBSTRATE_REPO) envsubst < helm/kagent/Chart-template.yaml > helm/kagent/Chart.yaml
458459
helm dependency update helm/kagent
459460
helm dependency update helm/kagent-crds
460461
helm package -d $(HELM_DIST_FOLDER) helm/kagent-crds

examples/substrate-openclaw/README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,37 @@ Follow these instructions to install Substrate on a kind cluster. This feature a
66

77
This assumes you've configured a kind cluster using `make create-kind-cluster`.
88

9-
Create a `substrate-values.yaml` file:
9+
### Image-pull args for a local (kind) registry
10+
11+
atelet is what pulls the ActorTemplate container image for each golden actor. It uses
12+
`go-containerregistry` directly (not containerd), so containerd's registry-mirror
13+
config on the kind node does **not** apply to atelet — you have to tell atelet how to
14+
reach the local kind registry with its own flags:
1015

1116
```yaml
1217
atelet:
18+
# Skip the GCP application-default-credentials probe. Substrate defaults this
19+
# to true (for GKE + Artifact Registry); on kind it just adds latency and a
20+
# noisy log line before falling back to anonymous auth.
21+
gcpAuthForImagePulls: false
22+
23+
# Rewrite `localhost:PORT/...` image refs (which is what `make helm-install`
24+
# renders when `--set registry=localhost:5001` is in effect) to a hostname
25+
# that atelet's puller can actually resolve from inside its pod. atelet also
26+
# applies `name.Insecure` for any ref that was originally `localhost:*`, so
27+
# the rewritten `kind-registry:5000` ref is fetched over HTTP (kind-registry
28+
# is `registry:2` with no TLS by default). Without both parts of this — the
29+
# rewrite AND the insecure flag it triggers — atelet errors out with either
30+
# `dial tcp [::1]:5001: connect: connection refused` (no rewrite), or
31+
# `http: server gave HTTP response to HTTPS client` (rewrite without Insecure).
1332
extraArgs:
1433
- --localhost-registry-replacement=kind-registry:5000
1534
```
1635
36+
When installing Substrate as a **subchart** of kagent (i.e. `--set substrate.enabled=true`
37+
on the kagent chart), prefix these keys with `substrate.` — e.g.
38+
`--set-json 'substrate.atelet.extraArgs=["--localhost-registry-replacement=kind-registry:5000"]'`.
39+
1740
Then install the Substrate platform and kagent:
1841

1942
```bash

go/api/httpapi/substrate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type SubstrateActorTemplateEntry struct {
3737
// SubstrateActorEntry is runtime state from ate-api (redis).
3838
type SubstrateActorEntry struct {
3939
ActorID string `json:"actorId"`
40+
Atespace string `json:"atespace,omitempty"`
4041
Status string `json:"status"`
4142
ActorTemplateNamespace string `json:"actorTemplateNamespace,omitempty"`
4243
ActorTemplateName string `json:"actorTemplateName,omitempty"`

go/core/internal/a2a/substrate_sandbox_transport.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (t *substrateSandboxSessionRoundTripper) RoundTrip(req *http.Request) (*htt
7272

7373
// Proxy the A2A request through atenet-router to the session actor. The router
7474
// selects the actor by HTTP Host; actor ID comes from EnsureSessionActor above.
75-
actorRT, err := newSubstrateAgentRoundTripper(t.routerURL, res.Handle.ID, t.base)
75+
actorRT, err := newSubstrateAgentRoundTripper(t.routerURL, res.Handle.Atespace, res.Handle.ID, t.base)
7676
if err != nil {
7777
return nil, err
7878
}

go/core/internal/a2a/substrate_transport.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ type substrateAgentRoundTripper struct {
1414
base http.RoundTripper
1515
}
1616

17-
func newSubstrateAgentRoundTripper(routerURL, actorID string, base http.RoundTripper) (http.RoundTripper, error) {
18-
target, host, err := substrate.GatewayRouterTarget(routerURL, actorID)
17+
func newSubstrateAgentRoundTripper(routerURL, atespace, actorID string, base http.RoundTripper) (http.RoundTripper, error) {
18+
target, host, err := substrate.GatewayRouterTarget(routerURL, atespace, actorID)
1919
if err != nil {
2020
return nil, err
2121
}

go/core/internal/httpserver/handlers/agentharness_gateway.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (h *Handlers) HandleAgentHarnessGateway(w ErrorResponseWriter, r *http.Requ
8383
return
8484
}
8585

86-
target, upstreamHost, err := h.resolveSubstrateGatewayTarget(r.Context(), ensureRes.Handle.ID)
86+
target, upstreamHost, err := h.resolveSubstrateGatewayTarget(r.Context(), ensureRes.Handle.Atespace, ensureRes.Handle.ID)
8787
if err != nil {
8888
log.Info("resolve substrate gateway target failed", "error", err)
8989
http.Error(w, err.Error(), http.StatusServiceUnavailable)
@@ -99,19 +99,20 @@ func (h *Handlers) HandleAgentHarnessGateway(w ErrorResponseWriter, r *http.Requ
9999
// explicitly suspends a session via the suspend endpoint.
100100
}
101101

102-
func (h *Handlers) resolveSubstrateGatewayTarget(ctx context.Context, actorID string) (*url.URL, string, error) {
102+
func (h *Handlers) resolveSubstrateGatewayTarget(ctx context.Context, atespace, actorID string) (*url.URL, string, error) {
103103
cfg := h.AgentHarnessGateway
104104
if cfg == nil {
105105
return nil, "", fmt.Errorf("substrate gateway is not configured")
106106
}
107107

108108
actorID = strings.TrimSpace(actorID)
109-
target, host, err := substrate.GatewayRouterTarget(cfg.AtenetRouterURL, actorID)
109+
target, host, err := substrate.GatewayRouterTarget(cfg.AtenetRouterURL, atespace, actorID)
110110
if err != nil {
111111
return nil, "", fmt.Errorf("substrate actor %q: %w", actorID, err)
112112
}
113113
ctrllog.FromContext(ctx).WithName("agentharness-gateway").Info(
114114
"proxying via atenet-router",
115+
"atespace", atespace,
115116
"actor", actorID,
116117
"router", target.String(),
117118
"host", host,

go/core/internal/httpserver/handlers/agentharness_gateway_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
func TestACPProxyForwardsToAtenetRouterWithActorHost(t *testing.T) {
1616
t.Parallel()
17-
const actorHost = "ahr-kagent-my-claw.actors.resources.substrate.ate.dev"
17+
const actorHost = "ahr-kagent-my-claw.kagent.actors.resources.substrate.ate.dev"
1818

1919
var gotHost, gotAuth, gotPath string
2020
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -56,9 +56,9 @@ func TestACPProxyForwardsToAtenetRouterWithActorHost(t *testing.T) {
5656

5757
func TestACPProxyRewriteTargetsAtenetRouterHost(t *testing.T) {
5858
t.Parallel()
59-
const actorHost = "ahr-kagent-my-claw.actors.resources.substrate.ate.dev"
59+
const actorHost = "ahr-kagent-my-claw.kagent.actors.resources.substrate.ate.dev"
6060

61-
target, host, err := substrate.GatewayRouterTarget(substrate.DefaultAtenetRouterURL, "ahr-kagent-my-claw")
61+
target, host, err := substrate.GatewayRouterTarget(substrate.DefaultAtenetRouterURL, "kagent", "ahr-kagent-my-claw")
6262
if err != nil {
6363
t.Fatal(err)
6464
}

go/core/internal/httpserver/handlers/substrate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ func (h *SubstrateHandler) listAteAPIState(ctx context.Context, namespaces []str
205205
func actorEntryFromPB(a *ateapipb.Actor) api.SubstrateActorEntry {
206206
return api.SubstrateActorEntry{
207207
ActorID: a.GetActorId(),
208+
Atespace: a.GetAtespace(),
208209
Status: substrate.ActorStatusLabel(a.GetStatus()),
209210
ActorTemplateNamespace: a.GetActorTemplateNamespace(),
210211
ActorTemplateName: a.GetActorTemplateName(),

go/core/pkg/sandboxbackend/async.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import (
99

1010
// Handle is the opaque identifier an AsyncBackend uses to address a sandbox
1111
// it owns on an external control plane. Persisted in AgentHarness.Status.BackendRef.
12+
//
13+
// For Substrate backends, Atespace scopes the ID — an actor's identity on
14+
// substrate is (Atespace, ID). Non-substrate backends may leave Atespace empty.
1215
type Handle struct {
13-
ID string
16+
ID string
17+
Atespace string
1418
}
1519

1620
// EnsureResult is returned by EnsureAgentHarness. Endpoint (if set) is surfaced

go/core/pkg/sandboxbackend/substrate/actor_reachability.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const (
1818
)
1919

2020
type actorGetter interface {
21-
GetActor(ctx context.Context, actorID string) (*ateapipb.Actor, error)
21+
GetActor(ctx context.Context, atespace, actorID string) (*ateapipb.Actor, error)
2222
}
2323

2424
// waitForActorReachableViaAtenet blocks until ate-api reports the actor RUNNING and
@@ -27,7 +27,7 @@ func waitForActorReachableViaAtenet(
2727
ctx context.Context,
2828
actors actorGetter,
2929
httpClient *http.Client,
30-
routerURL, actorID string,
30+
routerURL, atespace, actorID string,
3131
) error {
3232
if actors == nil {
3333
return fmt.Errorf("substrate ate-api client is required")
@@ -43,7 +43,7 @@ func waitForActorReachableViaAtenet(
4343
waitCtx, cancel := context.WithTimeout(ctx, defaultActorReachabilityTimeout)
4444
defer cancel()
4545

46-
target, host, err := GatewayRouterTarget(routerURL, actorID)
46+
target, host, err := GatewayRouterTarget(routerURL, atespace, actorID)
4747
if err != nil {
4848
return err
4949
}
@@ -53,7 +53,7 @@ func waitForActorReachableViaAtenet(
5353
defer ticker.Stop()
5454

5555
for {
56-
actor, getErr := actors.GetActor(waitCtx, actorID)
56+
actor, getErr := actors.GetActor(waitCtx, atespace, actorID)
5757
if getErr == nil && actor.GetStatus() == ateapipb.Actor_STATUS_RUNNING {
5858
statusCode, probeErr := probeActorViaAtenetRouter(waitCtx, httpClient, probeURL, host)
5959
if probeErr == nil && statusCode < http.StatusInternalServerError {

0 commit comments

Comments
 (0)