Agent Substrate
Agent Substrate runs each agent as an actor on a pool of warm Kubernetes workers, inside a gVisor or microVM sandbox. When an actor goes idle, Substrate snapshots its process memory and files and frees the worker; the next request restores it. A snapshot belongs to one actor and ends when the actor is deleted. This page puts Recall beside it: what the agent learned and where it was in its work live outside the actor, the actor holds no key for them, and an actor created again under the same name gets them back.
Before you start
- A cluster with Agent Substrate v1.0.0 or later installed. The example below uses the kind cluster from Substrate's quickstart.
recall0.25.0 or later, which adds-substrate-issuer.- A store for Recall. The example keeps memory in a local database inside the Recall pod; a real install names a backend you run.
How it works
An actor on Substrate has no credentials of its own. Every outbound request goes through Substrate's egress gateway, and an actor's egress policy can tell the gateway to replace a request header with a short-lived token that Substrate mints for that actor. Recall accepts that token directly:
- The Recall server is started with
-substrate-issuer, the URL of the install's token issuer. It trusts that issuer and no other. - The actor calls Recall with any placeholder in its
Authorizationheader. - The gateway discards the placeholder and sends the actor's token in its place.
- Recall checks the token's signature against the issuer's published keys, its audience, and its expiry, and reads which actor it names.
Everything in the caller's identity comes from Substrate, not from the actor:
| Part | Where it comes from |
|---|---|
| Tenant | The actor's atespace, Substrate's unit of tenancy, from the token's subject. |
| Agent | The actor's name, from the same subject. A request that names another agent is refused. |
| Memory | The Recall namespace <atespace>:<name>. With -substrate-scope atespace, the actors of one atespace share one namespace instead. |
There is no exchange for a Recall key. A key would sit in the actor's memory and be written into its snapshots; a token the gateway adds on the way out never enters the actor. The token names the actor and not one incarnation of it, so deleting an actor and creating it again under the same name reaches the same memory.
Run it
-
Start the Recall server with the issuer, and give it the CA that signs Substrate's in-cluster certificates so it can fetch the issuer's keys:
containers: - name: recall image: ghcr.io/polign/recall:0.25.0 args: - serve - -substrate-issuer=https://idp.ate-system.svc - -substrate-ca-file=/run/servicedns-ca/trust-bundle.pem volumeMounts: - {name: servicedns-ca, mountPath: /run/servicedns-ca, readOnly: true} volumes: - name: servicedns-ca projected: sources: - clusterTrustBundle: signerName: servicedns.podcert.ate.dev/identity labelSelector: matchLabels: {podcert.ate.dev/canarying: live} path: trust-bundle.pem -
Give the actor Recall's address and nothing else. In the
ActorTemplate:containers: - name: agent image: your-agent-image@sha256:... env: - name: RECALL_URL value: http://recall.recall-system.svc.cluster.local - name: RECALL_API_KEY value: placeholderThe Python client and the integrations built on it send
RECALL_API_KEYas the bearer, so the placeholder is all they need. -
Create the actor's egress policy. It allows Recall and has the gateway add the actor's token:
rules: - http: hostnames: ["recall.recall-system.svc.cluster.local"] effects: replaceHeaders: - header: Authorization prefix: "Bearer " actorJwt: audiences: ["recall"] expirationSeconds: 900kubectl ate create egress-policy my-agent -a my-atespace -f egress-policy.yaml
The complete example, with the server manifest, a worker pool, a demo actor, and a script that runs it on kind and checks the results, is in deploy/agent-substrate.
Hermes Agent on Substrate
The Recall memory provider for Hermes Agent runs
inside an actor without changes. It reads RECALL_URL and
RECALL_API_KEY from the actor's environment, as in step 2, and the egress
policy from step 3 does the rest. An example that drives the provider through a completed
turn, the start of the next turn, a tool call, a suspend, and a deleted actor is in
plugins/hermes/substrate.
Three things to know about actors
- Name the Service in full. An actor's DNS answers only the names its
egress policy lists, and it matches the name as it is looked up, which for a Service is
the fully qualified one. Use
recall.recall-system.svc.cluster.localin the policy; a policy that listsrecall.recall-system.svcgetsno such host. - Each actor has its own egress policy. A policy belongs to one actor and is not inherited from the template. An actor with no policy reaches nothing, Recall included, so create the policy when you create the actor.
- Connections do not survive a suspend. Substrate does not keep open network connections in a snapshot. The Python client opens a connection per call, so it works after a resume without being reopened.
What the first run showed
Run on a kind cluster on a laptop with Substrate v1.0.0 and gVisor workers, November 2026. The timing is from that laptop, with the snapshot on the same node, and will differ on a real cluster.
| What | Result |
|---|---|
| An actor remembers a fact and recalls it, holding no key | Recalled |
| A second actor asks the same question | Nothing returned |
| A call that reaches Recall without an actor token | 401 |
| Suspended actor: first request restores it and recalls from Recall | 0.1 to 0.3 s over seven runs, in-process state intact |
| Actor deleted and created again under the same name | New process, same memory |
| One actor resuming another actor's working state | 403 |
| Recreated actor resuming its own working state | The goal the deleted actor recorded came back |
Limitations
- A token stays valid until it expires. Substrate does not revoke actor tokens, so a deleted actor's last token works for the rest of its lifetime: fifteen minutes with the policy above, five at the shortest Substrate allows.
- Whoever may write an atespace's egress policies chooses where its actors' tokens are
sent. Recall accepts only tokens minted for its own audience,
recallby default, so a token minted for another service is refused. - An actor created again under a deleted actor's name finds its agent still resumed for
the old actor.
agent_resumeanswers409 already_resumed; the new actor callsagent_releaseand resumes, and gets the working state back. - Memory is per actor name. Two actors that should share what they learn need
-substrate-scope atespace, which applies to the whole server. - The
recallHelm chart has no settings for Substrate yet. Use the manifest in the example.
If something goes wrong
| What you see | What to do |
|---|---|
no such host for the Recall Service from inside an actor | The actor has no egress policy, or its policy does not list the name being looked up. List the fully qualified name, ending in .svc.cluster.local, and use the same name in RECALL_URL. A policy created a moment ago takes a few seconds to apply. |
401 unauthenticated from Recall | The token's audience is not the one the server expects. Set audiences: ["recall"] in the policy, or start the server with a matching -substrate-audience. |
401 from Recall and serve: substrate: lines in its log | The server could not fetch the issuer's keys. Check -substrate-issuer and that the trust bundle is mounted at -substrate-ca-file. The server tries again within five seconds. |
Actor template refused: must be pinned by digest | Substrate takes images by digest only. Use image@sha256:.... |
Resume refused with namespace_mismatch | The actor asked to resume an agent other than itself. Use the actor's own name as the agent_id. |
Source & license
The server and the example are part of Recall, open source under the Apache License 2.0, in github.com/Polign/recall. The contract for an actor's identity is in the State API, version 1.