Honest limits
This page lists what kslate does not do. Some of it is by design. Some of it is not built yet. Read it before you design a class or put kslate on a network you do not control.
What happens when things fail
| Failure | What happens |
|---|---|
| The gateway crashes during a write | The transaction rolls back. The agent gets a transport error and reads again. No committed write is lost. |
| Two gateways write the same field with the same expected version | One write wins. The other gets STALE. |
| Two workers claim the same task | One claim wins. The other gets HELD, with the owner and the lease expiry. |
| A worker dies while holding a claim | The lease runs out. The next claim succeeds and records took_over_from. Work done after the last checkpoint is lost, unless it was written to an append field. |
A worker dies holding a claim, and another worker’s write listed one of its fields in based_on | Nothing changes. based_on is about field versions, not about who holds a task. |
| Postgres is down | The gateway returns UNAVAILABLE. Nothing is cached or queued. Agents wait. |
| The Kubernetes TokenReview API is down | Tokens already in the cache keep working until they expire. New tokens are rejected. |
| The policy source is down | The last known classes stay in effect. The gateway logs the problem. |
| A class is deleted | Its records stay until their TTL. Writes get CLASS_GONE. Roles the class last named can still read. |
An agent lies in evidence | Not detected. kslate records who said what. It does not check whether it was true. |
| The world changes but nobody writes to the record | Not detected. kslate knows the record, not the cluster. |
After you delete a class
Deleting a class leaves its records behind. What tracks that is the cached copy of the class in Postgres. Nothing deletes that row, so “in the cache but not in the live policy source” is what “gone” means, and the last known class is exactly what kslate needs: which roles may still read, and what the TTL was.
- Writes get 410 Gone. Not 404, because the record does exist. Not 409 or 422, because there is nothing the caller can fix.
put,append,claimand acheckpointsave are all refused, for everyone who holds a role.- Reads keep working for every role the last known class named, not only
for an operator.
queryis a read and keeps working. The audit trail has to stay readable by the people the class named, and that class is the only record of who they were. - The worker keeps sweeping the records on the cached TTL, and drops the cached class once the last record is gone.
- A class with no TTL keeps its records, exactly as it would have alive, and keeps the cached row with them. “Until the TTL” never arrives for a class that never had one.
- The controller derives no Kubernetes RBAC for a deleted class. Its Role and RoleBinding went with the resource.
How long history lives
History goes with its record. At the TTL the record and its history are
deleted together, unless the class sets ttl: { onExpire: archive }, in
which case the snapshot carries the whole history.
So history is kept for the record’s lifetime, which is not the same as kept forever. A class whose history you want to keep sets that one line.
A rejected write that never created a record leaves a history row
belonging to no record. A STALE against a record that does not exist is
one. Those rows are in no record’s lifetime, so the worker reaps them
once they are a week old. Storage has the rest of
the retention.
What kslate does not check
- Evidence is not checked for truth. kslate checks that evidence is present. If an agent cites a log line that does not exist, kslate records that it cited it. A check that a claim follows from its evidence is planned for a later version. It is not built.
based_onis not checked for completeness. If an agent relies on a field but does not declare it inbased_on, kslate accepts the write. Inferredbased_oncovers this for agents that send the scope header. An agent that sends neither gets nobased_oncheck.kslate verifyflags writes with nobased_onafter the fact.- A rejection does not force a re-decision. kslate guarantees the
rejected write did not land and is in the history. It cannot make the
caller think again. A model that gets a 409 through a tool call may
ignore it or retry it unchanged.
kslate verifyflags the unchanged retry. - Checkpoints are not verified. A checkpoint is the agent’s own account of its progress. kslate stores it and reports which fields changed since. It does not compare the account to the record.
- Nothing is pushed. A lease running out, a field changing, a takeover: none of these notify anyone. An agent finds out by reading or claiming.
What kslate does not do
kslate rules these out by design:
| conflict resolution | checking the world |
| orchestration | task decomposition |
| message passing between agents | multi-field atomic writes |
| vector search | LLM extraction or summarisation |
| a UI | multi-cluster |
| a hosted service | CRDTs |
| a custom storage engine | multi-tenancy beyond namespaces |
What that means when you design a class:
One field per write. Two fields that must change together cannot. No measurement exists of how often that bites.
queryis exact match only. It filters by field equality and returns the oldest records first, one page at a time. Follownext_afterfor the rest. There is no ordering by which record changed least recently, so a long backlog is worked in creation order.That is decided, not missing. “Stale” means behind the version a particular worker last acted on, and that marker lives in the worker’s own checkpoint, not anywhere the gateway can see. Sort the candidates yourself. One
queryresponse carries every version you need.The Go sidecar is not an A2A implementation. Its exchange with the agent container is one JSON-RPC request per cycle with a small response shape. That is not the full A2A protocol. The Python client’s
run(Agent(...))is a supported adapter. See Clients.
Not built yet
- No container images are published. You build them yourself.
- Client-certificate identity (
mtls) works only on the HTTP listener and only when the gateway terminates TLS itself. Behind an ingress that terminates TLS there is no certificate at the gateway, and kslate will not trust a header in its place. gRPC and MCP take a bearer token. - The worker’s lease sweeper only reports. Expiry is enforced when an agent claims, so nothing is lost. But no component reacts to a lapsed lease on its own.
Security
- Traffic is plaintext by default. HTTP/JSON, gRPC and MCP are served
without TLS. To change that, pass
--tls-certand--tls-keytoserve, or settls.enabledin the chart. Anyone on a plaintext hop can read a bearer token. Run behind a mesh or a TLS-terminating ingress if you do not enable TLS. - The MCP path uses a long-lived API key. kagent’s
RemoteMCPServercannot present a projected ServiceAccount token, so the key lives in a Secret. The sidecar path uses a rotating, cluster-verified token. - Unauthenticated callers are rate-limited, not blocked. A bad
credential costs one identity lookup and one log line. Each remote
address gets one budget of rejected credentials, shared by HTTP, gRPC
and MCP. For IPv6, each /64 is one address. The default is 50 per
second with a burst of 200. After that the address gets
RATE_LIMITED(HTTP 429) before any identity lookup. Behind a shared proxy, every caller shares one address. There is no per-identity rate limit on authenticated writes. One request is bounded by the 4 MiB body limit, the 256-byte id limit andbased_onde-duplication. The total is not bounded: a role that may write one field can grow that record’s history for as long as the record lives. - One thing leaks whether a class exists. An unknown class returns 404. A class that exists returns 403 to a caller with no role in it.
- Metric labels name classes, fields and roles. That is your policy
graph. Metrics are on their own port and Service. The chart’s
networkPolicycan restrict who scrapes them.
To report a vulnerability, open a private security advisory on the repository. Before 1.0, only the latest release gets fixes. See Contributing for what to expect.
Operations
- The bundled Postgres is one pod with no replica, on an
emptyDir. Its data does not survive a restart. Use it for development only. - Gateways are stateless. You can run as many as you like. They all share one Postgres, and every write to a record locks that record’s row. The cost of that lock is what the deferred performance work will measure.
- Class names are shared across the whole cluster. A second class with
the same name in another namespace is refused with
Ready=FalseandDuplicateClassName. SetwatchNamespaceif several teams create classes. - Changing a class does not migrate existing records.
expires_atis fixed when a record is created. A field you remove stays readable, and writes to it getUNKNOWN_FIELD. A mode change is not checked against existing values. - Inferred
based_oncomes from reads in the last 15 minutes. The worker deletes read records after an hour. If one scope header is used for longer than a task, inference quietly stops.