Skip to content
Honest limits

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

FailureWhat happens
The gateway crashes during a writeThe 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 versionOne write wins. The other gets STALE.
Two workers claim the same taskOne claim wins. The other gets HELD, with the owner and the lease expiry.
A worker dies while holding a claimThe 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_onNothing changes. based_on is about field versions, not about who holds a task.
Postgres is downThe gateway returns UNAVAILABLE. Nothing is cached or queued. Agents wait.
The Kubernetes TokenReview API is downTokens already in the cache keep working until they expire. New tokens are rejected.
The policy source is downThe last known classes stay in effect. The gateway logs the problem.
A class is deletedIts records stay until their TTL. Writes get CLASS_GONE. Roles the class last named can still read.
An agent lies in evidenceNot detected. kslate records who said what. It does not check whether it was true.
The world changes but nobody writes to the recordNot 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, claim and a checkpoint save 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. query is 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_on is not checked for completeness. If an agent relies on a field but does not declare it in based_on, kslate accepts the write. Inferred based_on covers this for agents that send the scope header. An agent that sends neither gets no based_on check. kslate verify flags writes with no based_on after 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 verify flags 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 resolutionchecking the world
orchestrationtask decomposition
message passing between agentsmulti-field atomic writes
vector searchLLM extraction or summarisation
a UImulti-cluster
a hosted serviceCRDTs
a custom storage enginemulti-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.

  • query is exact match only. It filters by field equality and returns the oldest records first, one page at a time. Follow next_after for 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 query response 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 latency or throughput number on this site has been measured. The target is p99 write latency at or under 50 ms with 50 agents writing one record at once. The load harness and the OpenTelemetry spans are not built. Prometheus metrics are.
The cost-of-governance run and the fault-injection results do not exist yet. No measurement is published on this site.
  • 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-cert and --tls-key to serve, or set tls.enabled in 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 RemoteMCPServer cannot 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 and based_on de-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 networkPolicy can 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=False and DuplicateClassName. Set watchNamespace if several teams create classes.
  • Changing a class does not migrate existing records. expires_at is fixed when a record is created. A field you remove stays readable, and writes to it get UNKNOWN_FIELD. A mode change is not checked against existing values.
  • Inferred based_on comes 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.