blame
kslate keeps a history of every write to a record, including the ones it
rejected. Each row says who made the write, what they sent, what they
cited as evidence, what they based it on, and whether it was applied.
kslate blame shows that history for one field. kslate history shows
it for the whole record.
Both read through the gateway, so you only see fields your role may read.
Every CLI example below omits the connection flags for readability. The full form is:
KSLATE_APIKEY=$TOKEN kslate blame --server http://127.0.0.1:8080 incidents/inc-4471 planned_actionWho changed this field
$ kslate blame incidents/inc-4471 planned_action
planned_action (versioned, evidence required)
v4 22:00:39 apikey:diagnoser "fail over done; scale payments-api to 8"
evidence: hypotheses@2
v3 22:00:36 apikey:diagnoser "fail over, then scale payments-api to 6"
evidence: hypotheses@2
based_on: hypotheses@2
superseded by v4 after 2s
v2 22:00:36 apikey:diagnoser "fail over payments-db to replica 2"
evidence: hypotheses@2
based_on: hypotheses@2
superseded by v3 after 0s
v1 22:00:36 apikey:diagnoser "restart payments-api pods"
evidence: hypotheses@1
based_on: hypotheses@1
superseded by v2 after 0s
-- 22:00:36 apikey:remediator REJECTED STALE_PREMISE
write: actions_taken based_on: planned_action@2 (moved to v3 at 22:00:36) (inferred)
-- 22:00:36 apikey:remediator REJECTED STALE_PREMISE
write: actions_taken based_on: planned_action@1 (moved to v2 at 22:00:36)Read it top to bottom. Each version of the field, newest first, with who
wrote it, what evidence they gave, what they based it on, and how long it
stood. Then the rejected writes that involved this field. The last two
rows are writes to actions_taken that were refused because they relied
on a version of planned_action that had already changed. (inferred)
means kslate worked out the based_on from the caller’s own reads. See
based_on.
Other rejections appear the same way:
$ kslate blame incidents/inc-4471 status
status (versioned)
v3 22:00:39 apikey:detector "resolved"
v2 22:00:36 apikey:detector "mitigating"
superseded by v3 after 3s
v1 22:00:36 apikey:detector "open"
superseded by v2 after 0s
-- 22:00:39 apikey:communicator REJECTED SCOPE_DENIED
expected_version: 3
-- 22:00:36 apikey:remediator REJECTED STALE
expected_version: 1The communicator tried to write a field its role may not write. The remediator tried to write with a version that was already old.
Everything that happened to a record
kslate history lists every write to the record, newest first. This
excerpt shows a task being claimed, released, re-claimed, and taken over,
plus one rejected write:
$ kslate history incidents/inc-4471
v5 22:00:39 remediation apikey:remediator takeover APPLIED
v4 22:00:36 remediation apikey:remediator2 claim APPLIED
v3 22:00:36 remediation apikey:remediator release APPLIED
v2 22:00:36 remediation apikey:remediator claim APPLIED
-- 22:00:36 remediation apikey:remediator2 claim HELD
v1 22:00:36 remediation apikey:remediator claim APPLIED
v3 22:00:36 actions_taken apikey:remediator append APPLIED
evidence: kubectl rollout status
-- 22:00:36 actions_taken apikey:remediator append STALE_PREMISE
evidence: kubectl scale
based_on: planned_action@2 (inferred)Check the agents’ behaviour
kslate verify reads the same history and reports which rejections fired
and which roles behaved oddly or never wrote at all. A class whose roles
did not all write produces one finding per silent role, which is normal
for a record only two of them touched:
incidents/inc-1
guarantees fired:
STALE_PREMISE 1
findings: 4
ROLE_WROTE_NOTHING communicator
no identity of this role has written to the record
ROLE_WROTE_NOTHING detector
no identity of this role has written to the record
ROLE_WROTE_NOTHING dev
no identity of this role has written to the record
ROLE_WROTE_NOTHING operator
no identity of this role has written to the recordverify exits non-zero whenever it has findings, so a script that calls
it should expect that.
What a history row contains
| Column | What it holds |
|---|---|
seq | A number that increases with every write, across every record |
field, op | The field, and put, append, claim, release or takeover |
version | The field’s version after the write, when it was applied |
record_version | The record’s version. After the write when applied, at the moment of refusal when rejected |
value | The value written, or the value the caller tried to write. A scope, evidence or identity rejection keeps at most 64 KiB of it |
author | The identity kslate verified on the connection |
identity_strength | How well that credential was bound: pod-bound, sa-bound, issuer-declared or shared-secret. Absent on rows written before the gateway recorded it |
invocation | The writer’s own statement of which call produced the write, when it sent one. kslate does not verify it |
evidence | What the caller cited |
expected_version, based_on | What the caller sent. inferred: true if kslate derived based_on |
outcome | applied, stale, stale_premise, held, scope_denied, weak_identity, evidence_required or identity_mismatch |
created_at | When |
Only failures the gateway reached the record to decide leave a row:
stale, stale_premise, held, scope_denied, weak_identity,
evidence_required and identity_mismatch.
A request rejected before that point writes nothing. These never reach history:
- a bad credential
- an identity with no role
- an unknown field
- the wrong operation for the field’s mode
- a malformed body
- a failed checkpoint gate
The endpoint
GET /v1/records/{class}/{id}/history?field=planned_action&limit=1000&before=Rows come newest first, 1000 per page by default. When there are more,
the response includes next_before. Pass it back as before for the
next page. Each row is the history record above as JSON.
History has no MCP tool and no gRPC method. That is a decision, not a
gap. An agent acts on the record’s current fields and versions, which
get returns. The audit trail is there so a person can check afterwards what the agents
did. It stays a gateway route rather than a table you read directly. A
direct read would bypass scope.
What history tells you, and what it cannot
History answers four questions:
- who wrote this value, and when
- what evidence they cited
- which versions of which other fields they relied on
- which writes were refused, and for what reason
It does not tell you whether the evidence was true. If an agent cites a log line that does not exist, history records that it cited it. Checking the world is your job.
History is kept as long as the record lives. When the record expires, the
history is deleted with it, or archived with it if the class says
onExpire: archive.
(fence) and (inferred)
Two marks can appear on a based_on line.
(inferred) means kslate derived the premise from the reads the caller
made rather than the caller declaring it (based_on).
(fence) means the writer declared that premise to prove it still held a
claim, not because its decision relied on the value. Read it as a lease
check, not as provenance — the writer never looked at that field’s
contents. kslate-reconciler marks its pre-write claim check this way.