kagent
You can connect a kagent Agent to a kslate record in two ways. With the sidecar, a small container runs next to your agent and does all the kslate work for it. With MCP, the model calls kslate tools directly. The two paths enforce different things, so pick deliberately.
Sidecar (kslate-reconciler) | MCP (RemoteMCPServer) | |
|---|---|---|
| Changes to your agent | No code change. Its prompt has to ask for the response shape below. | No code change. Its prompt should say what to do on a rejection. |
| How kslate knows who is calling | The pod’s ServiceAccount token, checked with the Kubernetes API | An API key stored in a Secret |
How based_on is set | The sidecar declares them from the record it just read | kslate infers them from a scope header, if kagent passes it through |
| What happens after a rejection | The sidecar reads again and re-runs the agent. If it loses its lease it drops the result. | The model decides. kslate cannot make it re-read. |
| What kslate enforces | Version, based_on, lease, scope, identity, checkpoint | Version, scope, identity, and based_on when the header is present |
Both paths were tested against kagent 0.10.2 with kagent.dev/v1alpha2.
Install kslate first (Install). The gateway
Service is kslate-gateway.kslate.svc.
Path 1: the sidecar
A kagent Agent with type: BYO runs your own container image. You add
the sidecar to the same pod with spec.byo.deployment.extraContainers.
That field is part of the kagent CRD, so you do not patch the Deployment
kagent manages.
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: kslate-remediator
namespace: kagent
spec:
type: BYO
description: Incident remediator; coordination is handled by the kslate-reconciler sidecar, not by this agent.
byo:
deployment:
image: your-agent-image:tag # serves A2A on 8080
replicas: 1
serviceAccountName: remediator # → sa:kagent/remediator in the class
volumes:
- name: kslate-token
projected:
sources:
- serviceAccountToken:
audience: kslate
expirationSeconds: 3600
path: token
extraContainers:
- name: kslate-reconciler
image: kslate-reconciler:dev
imagePullPolicy: Never
args:
- --server=http://kslate-gateway.kslate.svc
- --token-file=/var/run/secrets/kslate/token
- --record=incidents/inc-42
- --claim-field=remediation
- --watch-field=planned_action
- --lease-seconds=20
- --a2a-endpoint=http://localhost:8080
- --interval=3s
volumeMounts:
- name: kslate-token
mountPath: /var/run/secrets/kslate
readOnly: trueThis manifest is deploy/examples/kagent-remediator-agent.yaml. Build the
sidecar image with docker build -f Dockerfile.reconciler -t kslate-reconciler:dev ., then load it into your cluster or push it to a
registry.
What the sidecar does
Every few seconds (--interval), the sidecar runs one cycle:
- It reads the record. If the watch field has not changed since the last time it acted, it stops here. Reading is free. Claiming writes a history row, so the sidecar does not claim until there is work.
- It claims the task field. If another replica holds it, the claim
returns
HELDand the sidecar stops. - It loads the role’s last checkpoint. If the checkpoint says this version was already handled, it releases the claim and stops. This is what keeps a replacement from repeating an action after a takeover.
- It reads the record again, because the plan may have changed between steps 1 and 2.
- It sends one JSON-RPC
message/sendrequest to your agent container. While your agent works, the sidecar keeps renewing the lease. If a renewal returnsHELD, another replica has taken over and the sidecar cancels the request. The request times out after--a2a-timeout(default 5 minutes). - It writes your agent’s answer to the record with
based_onset to the watch field at the version it read. If the plan changed while your agent was working, kslate rejects the write withSTALE_PREMISEand the next cycle starts over. - It saves a checkpoint and releases the lease.
What your agent receives and returns
The sidecar does not decide what to write. Your agent does. The sidecar hands it the record and takes back a list of writes.
The request. One JSON-RPC call, method message/send. The params are
the record name and every field the sidecar’s identity may read, in the
same shape get returns:
{"jsonrpc": "2.0", "id": 1, "method": "message/send", "params": {
"record": "incidents/inc-42",
"fields": {
"planned_action": {
"value": "roll back api deployment",
"version": 5,
"author": "sa:kagent/diagnoser",
"evidence": "api logs show config parse error at 14:02:11",
"updated_at": "2026-10-02T18:42:20Z"
},
"actions_taken": {
"version": 2,
"entries": [ { "seq": 7, "value": "restarted payments-api pods", "author": "sa:kagent/remediator", "evidence": "kubectl rollout restart", "updated_at": "..." } ]
},
"status": { "value": "mitigating", "version": 2, "author": "sa:kagent/detector", "updated_at": "..." }
}
}}Your agent reads fields, does its work, and decides what to record.
The response. The writes it wants made. This is a small subset of the A2A protocol, not the whole thing.
{"jsonrpc": "2.0", "id": 1, "result": {
"writes": [
{"op": "append", "field": "actions_taken", "entry": "rolled back the deployment", "evidence": "executed the plan"}
]
}}Each write has op (put or append), field, a value (for put) or
entry (for append), and evidence. An empty writes list means
“nothing to do”. The agent never mentions a version, a lease, or
based_on.
What the sidecar adds before writing:
based_on: the watch field at the version it read before calling your agent. If the plan changed during the call, kslate rejects the write withSTALE_PREMISE.expected_versionon aput: the version it read, or 0 if the field did not exist. Your agent can setexpected_versionitself, but normally should not.- A final lease renewal. If that returns
HELD, another replica took the task during the call and the sidecar discards your agent’s answer unwritten.
On a rejection. If a write is rejected with STALE_PREMISE and none
of this decision’s writes have landed yet, the sidecar reads the record
again. It calls your agent again with the new fields, up to a retry
limit.
If an earlier write in the same decision already landed, it stops and logs instead. kslate has no multi-field atomic write, so running the decision again could record an append twice. Return one write per decision when you can.
The checkpoint. After the writes land, the sidecar saves a checkpoint
with last_acted_version (the watch field version it acted on) and the
sections the class requires. A replacement reads it first, so it does not
act on the same version twice.
Your agent’s code does not change. Its prompt does: wherever you prompt
the agent, you tell it what fields contains and ask it to answer in the
response shape above.
Flags
| Flag | Meaning |
|---|---|
--server | The gateway URL. |
--token-file | File holding the bearer token. Read again on every request, so a rotated projected token is picked up. |
--apikey | Bearer token, for a gateway running with --identity apikey. |
--record | Watch one record, given as class/id. |
--class, --filter field=value, --concurrency | Watch every record in a class that matches the filters. The claim decides which replica handles each one. |
--claim-field | The task field to claim before acting. |
--watch-field | The field whose version change means there is work to do. |
--lease-seconds | How long each claim lasts. Default 300. |
--a2a-endpoint, --a2a-timeout | Where your agent container listens, and how long to wait for it. Defaults http://localhost:8080 and 5 minutes. |
--checkpoint-role | Which role to checkpoint as, if the identity holds more than one. |
--interval | How often to run a cycle. Default 5 seconds. |
The flags for reaching an agent through kagent 1.0’s gateway
(--transport kagent-gateway, --kagent-gateway, --agent,
--kagent-user-id, --task-encoding, --instruction,
--verify-contract, --stalled-backoff) are in the table under
Path 1 on kagent 1.0 below. The CLI reference
lists every flag.
Identity
Every pod of one Agent uses the same ServiceAccount, so kslate sees them as one identity. If you restart an Agent, the new pod re-claims as the same identity. That is not a takeover. To see a takeover, run two Agents with two ServiceAccounts, both named in the class’s role.
Path 1 on kagent 1.0: the reconciler as its own Deployment
On kagent 1.0 an agent runs as an Actor inside a shared Agent Substrate
worker pod. There is no second container slot, and the Actor has no
ServiceAccount token to present. So kslate-reconciler runs as its own
Deployment and reaches the agent through kagent’s A2A gateway. The loop is
the same code as the sidecar. The transport flags change.
args:
- --server=http://kslate-gateway.kslate.svc
- --token-file=/var/run/secrets/kslate/token
- --transport=kagent-gateway
- --kagent-gateway=http://kagent-controller.kagent.svc:8083
- --agent=kagent/kslate-remediator-agent
- --record=incidents/inc-42| Flag | |
|---|---|
--transport=kagent-gateway | Call the agent through kagent’s gateway instead of localhost:8080. |
--kagent-gateway | kagent’s controller base URL. |
--agent | The kagent Agent to invoke, as namespace/name. |
--task-encoding | How the task reaches the agent: text (default), data or both. kagent’s own runtime drops data parts before the model sees them. A BYO Actor receives them intact. |
--verify-contract | auto (default), require or skip. At startup the reconciler fetches the gateway’s gRPC descriptors and checks the fields it depends on. kagent’s A2A service is a fork of the public one, and a mismatch would otherwise fail silently. |
--kagent-user-id | The X-User-Id header sent to kagent. A label. kagent does not verify it. |
--instruction | Text put in front of the task. Empty by default. Needs a text part, so it cannot be combined with --task-encoding data; the reconciler refuses to start. |
--stalled-backoff | How long to leave a record alone after the agent asked for input. Only the kagent-gateway transport reports that state; on the sidecar transport this flag has no effect. |
The reconciler’s own ServiceAccount is the writer. Give it a projected
token with audience kslate, as for the sidecar, and list
sa:<namespace>/<name> in the class role. One ServiceAccount per agent:
two agents sharing one are one identity to kslate.
An agent that answers with INPUT_REQUIRED ends the cycle. The reconciler
does not answer questions. It leaves the record alone for
--stalled-backoff, then tries again. Write the agent’s prompt so that it
decides instead of asking.
The full manifest, with the ServiceAccount and the token mount, is
deploy/examples/kagent1-reconciler-deployment.yaml in the repository.
Path 2: MCP
The gateway serves MCP over streamable HTTP at /mcp on the API port. It
exposes six tools: kslate_get, kslate_put, kslate_append,
kslate_claim, kslate_checkpoint and kslate_query. They run the same
checks and return the same errors as the HTTP API.
This section covers the kagent wiring. For any other MCP client, or to
try the endpoint with curl, see Any MCP client.
apiVersion: v1
kind: Secret
metadata:
name: kslate-mcp-credential
namespace: kagent
type: Opaque
stringData:
authorization: "Bearer <a key from the gateway's --apikeys file>"
---
apiVersion: kagent.dev/v1alpha2
kind: RemoteMCPServer
metadata:
name: kslate
namespace: kagent
spec:
description: kslate shared context - the six operations of the kslate gateway.
url: http://kslate-gateway.kslate.svc/mcp
protocol: STREAMABLE_HTTP
timeout: 30s
headersFrom:
- name: Authorization
valueFrom:
type: Secret
name: kslate-mcp-credential
key: authorization
---
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: kslate-mcp-diagnoser
namespace: kagent
spec:
type: Declarative
declarative:
modelConfig: default-model-config
systemMessage: |
...read a versioned field before writing it and pass the version as
expected_version; on STALE, re-read and decide again; on
STALE_PREMISE, re-read the moved field first; never invent evidence.
tools:
- type: McpServer
mcpServer:
name: kslate
kind: RemoteMCPServer
apiGroup: kagent.dev
toolNames: [kslate_get, kslate_put, kslate_append, kslate_query]
allowedHeaders:
- X-Kslate-ScopeThe full manifest, including the complete system message, is
deploy/examples/kagent-mcp-agent.yaml.
Two things about the kagent CRD shape this path:
- The credential is an API key.
RemoteMCPServer.spec.headersFromcan only read a header value from a Secret or a ConfigMap. It cannot read the pod’s projected ServiceAccount token. So the gateway that serves MCP agents has to run with--identity apikey, and you are responsible for keeping the key safe. A long-lived key in a Secret is weaker than a token that rotates every hour. The Helm chart does not install such a gateway: it starts one with--identity k8s-sa. Take this path against a gateway you run yourself. based_oninference needsallowedHeaders. Whoever invokes the Agent setsX-Kslate-Scopeon the request. kagent passes it to every tool call. kslate then treats whatever that header value read as thebased_onof whatever it writes. See based_on. The model never sees the header. If you leave outallowedHeaders, kslate still records every write in history but no longer checksbased_on. Nothing in the agent or its transcript shows you the difference.
Use one scope header value per task or conversation, at most 256 bytes. Do not use the MCP session id. One session covers unrelated conversations.
Choosing a path
Use the sidecar when you need coordination. Its loop is code. It reads again after a rejection and stands down when it loses a lease.
Use MCP when you need a governed record. kslate still enforces identity,
evidence, scope and history no matter what the model does, and it checks
based_on when the header is present. But kslate cannot make the model
re-read after a rejection. The model may ignore a 409 or retry the same
write. kslate only guarantees that the rejected write did not land and
that the attempt is in history.
A runnable demo of the sidecar path is in a companion repository. It includes a stub agent, a standby Agent for the takeover, and three test arms.
Which identity each path gets
The path you pick decides what kslate can verify about the writer. That sets the identity strength on every write, and the floors a class can usefully require.
| path | where the adapter runs | writer identity | strength |
|---|---|---|---|
| kagent 0.x sidecar | a second container in the agent’s own pod | the pod’s ServiceAccount, via TokenReview | pod-bound, or sa-bound on a cluster that does not report the binding |
| kagent 1.0 gateway | its own Deployment, one per agent | the reconciler’s own ServiceAccount, via TokenReview | same |
| Python adapter in your process | wherever your process runs | whatever that process presents | pod-bound or sa-bound in a pod; issuer-declared with OIDC |
| CI or a laptop | a standalone reconciler | an OIDC token, or an API key | issuer-declared, or shared-secret for a key |
| MCP | the model calls kslate directly | whatever credential the MCP server was given | usually shared-secret |
Two consequences follow.
A strong strength does not mean the agent decided it. On kagent 0.x
the sidecar’s token belongs to the pod that also runs the agent.
sa:kagent/remediator meant “this write came from the agent’s own pod”.
On kagent 1.0 the reconciler runs in its own Deployment, so the same
identity means “a Deployment whose flags name that agent”. The write is
just as authenticated either way. The link between the decision and the
writer is now configuration. The invocation object is what records which
agent call produced the write.
The MCP path is the weakest of the five. The credential has to live
where the MCP server can reach it. That is usually a key, not a bound
token. If a class sets a floor above shared-secret, the MCP path
cannot write those fields at all.