Skip to main content
Glama

sysknife_audit_verify

Verify the Ed25519-signed audit log hash chain to detect tampering. Returns intact, broken, or cannot_verify status with rows checked, plus censused and attributed counters.

Instructions

Verify the tamper-evident Ed25519-signed hash chain over the audit log. Returns status (intact/broken/cannot_verify), rows_checked, and, on broken, the first offending row. Also returns rows_censused and attributed_rows: intact is a statement about tampering, not about how much the trail can tell you, so report those counters alongside the status rather than reading intact as complete attribution. Read-only and safe to call without prior sysknife_plan.

Input Schema

NameRequiredDescriptionDefault

No arguments

Output Schema

NameRequiredDescriptionDefault
actualNoThe hex Ed25519 signature actually stored for the first broken row.
reasonNoWhy the transaction chain could not be verified. Preserved even when another check proves a break and sets the headline to `"broken"`.
statusYesOne of `"intact"`, `"broken"`, `"cannot_verify"`.
backendYesBackend label: a filesystem path for SQLite, the literal `"postgres"` for Postgres deployments.
expectedNoWhat verification expected for the first broken row (the literal `"valid ed25519 signature"`).
audit_anchorYesThe configured checkpoint-anchor verdict, or the same unconfigured truncation caveat emitted by the CLI.
chain_statusYesThe transaction chain's own verdict: `"intact"`, `"broken"` or `"cannot_verify"`. Reported separately because `status` is the worst of three checks, so a broken *approval-event* chain sets `status` to `"broken"` while this stays `"intact"`. Read this one, not `status`, to decide whether the attribution counts below are findings or claims: without it an agent had no way to recover the chain verdict and would discard sound attribution.
rows_checkedYesNumber of audit rows the verifier successfully checked. `0` for `cannot_verify` outcomes that fail before the first row is read.
status_checkNoWhether live execution statuses agree with their signed events. `null` when the check did not run (including Postgres); an inconclusive SQLite check includes its reason and contributes to the headline verdict.
rows_censusedNoHow many rows were censused for attribution: every row read, whether or not it verified. `null` when the store could not be read at all, along with every count below, so a database nobody could open never reads as one where nothing was found. A readable but empty store reports `0`. When `chain_status` is not `"intact"` this can exceed `rows_checked`, and the difference is the part of the trail that was counted but not proven.
binding_statusYes`"consistent"`, `"not_checked"`, or `"missing_event"`: whether every event tip committed by a transaction row is still present in the event chain.
events_checkedYesNumber of approval events (grant / consume / revoke) checked in the second chain.
attributed_rowsNoHow many rows have a signed principal naming an account: a non-empty value under the `uid` or `token` scheme, which this build could read back as something the daemon itself could have written. Only a finding when `chain_status` is `"intact"`. Past a detected break the walk stopped checking, so those rows' principals are claims: some may be authentic, since deleting or reordering a row breaks the link while leaving later signatures valid, and this tool cannot say which.
rows_unattestedNoHow many rows have no principal any signature vouches for: the column is populated on an encoding that does not sign it, or holds a value this build cannot read back as one the daemon could have written, or the row declares an encoding this build does not know. This build writes none of those. The first two are out-of-band writes to investigate; the third means a newer SysKnife wrote the rows and the fix is to verify with a build at least that new.
audit_store_noteNoExplains when the verifier selected the system daemon's store because no per-user store exists. Omitted when no fallback explanation is needed.
first_broken_seqNoSequence number of the first row that broke the chain. Only set when `status == "broken"`.
unattributed_rowsNoHow many rows record that the daemon could not name the caller. `chain_status: "intact"` with a non-zero count here means the chain is sound and the attribution is not: report both, never the first alone. Since 0.4.0 this counts only rows whose `chain_version = 3` principal is signed as `none:unattributed`. 0.3.0 matched the column on any encoding, which meant an unsigned column could land here; such rows are now `rows_unattested`.
daemon_socket_caveatNoSet when `SYSKNIFE_SOCKET` names a daemon that may not live on this machine, because verification reads a local store while every other tool travels over that socket. `None` for the local-daemon case.
approval_events_statusYesResult of the approval-event chain walk: `"intact"`, `"broken"`, or `"cannot_verify"`. Reported separately from `status` so a clean authorisation trail can never paper over a tampered approval trail.
rows_naming_no_accountNoHow many rows name no account, for any reason. The complement of `attributed_rows` over `rows_censused`, provided so a reader does not have to add the three reasons and risk missing one.
rows_without_principalNoHow many rows carry no principal the signature covers, normally because they were signed before the column existed. Reported next to `unattributed_rows` because zero attribution failures over a pre-v3 database would otherwise read as full attribution. The two have different remedies: this one cannot be fixed, since backfilling a principal would rewrite the bytes the signature covers.
first_broken_transaction_idNoTransaction ID of the first broken row. Only set when `status == "broken"`.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed7 schema fields changedv0.27.1
    • addedOutput schema / $defs
      Added value: +{
      +  "StatusCheckReport": {
      +    "description": "The SQLite execution-status cross-check, matching the CLI JSON encoding.",
      +    "oneOf": [
      +      {
      +        "properties": {
      +          "rows_checked": {
      +            "format": "uint64",
      +            "minimum": 0,
      +            "type": "integer"
      +          },
      +          "state": {
      +            "const": "agrees",
      +            "type": "string"
      +          }
      +        },
      +        "required": [
      +          "state",
      +          "rows_checked"
      +        ],
      +        "type": "object"
      +      },
      +      {
      +        "properties": {
      +          "detail": {
      +            "type": "string"
      +          },
      +          "state": {
      +            "const": "disagrees",
      +            "type": "string"
      +          }
      +        },
      +        "required": [
      +          "state",
      +          "detail"
      +        ],
      +        "type": "object"
      +      },
      +      {
      +        "properties": {
      +          "detail": {
      +            "type": "string"
      +          },
      +          "state": {
      +            "const": "cannot_verify",
      +            "type": "string"
      +          }
      +        },
      +        "required": [
      +          "state",
      +          "detail"
      +        ],
      +        "type": "object"
      +      }
      +    ]
      +  }
      +}
    • addedOutput schema / properties / audit_anchor
      Added value: +{
      +  "description": "The configured checkpoint-anchor verdict, or the same unconfigured\ntruncation caveat emitted by the CLI."
      +}
    • addedOutput schema / properties / audit_store_note
      Added value: +{
      +  "description": "Explains when the verifier selected the system daemon's store because\nno per-user store exists. Omitted when no fallback explanation is needed.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • changedOutput schema / properties / binding_status / description
      Previous value: -"`\"consistent\"` or `\"missing_event\"`: whether every event tip committed\nby a transaction row is still present in the event chain."New value: +"`\"consistent\"`, `\"not_checked\"`, or `\"missing_event\"`: whether every\nevent tip committed by a transaction row is still present in the event\nchain."
    • changedOutput schema / properties / reason / description
      Previous value: -"Human-readable explanation. Only set when `status == \"cannot_verify\"`."New value: +"Why the transaction chain could not be verified. Preserved even when\nanother check proves a break and sets the headline to `\"broken\"`."
    • addedOutput schema / properties / status_check
      Added value: +{
      +  "anyOf": [
      +    {
      +      "$ref": "#/$defs/StatusCheckReport"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "description": "Whether live execution statuses agree with their signed events.\n`null` when the check did not run (including Postgres); an inconclusive\nSQLite check includes its reason and contributes to the headline verdict."
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "status",
      -  "rows_checked",
      -  "events_checked",
      -  "approval_events_status",
      -  "binding_status",
      -  "backend",
      -  "chain_status"
      -]New value: +[
      +  "status",
      +  "rows_checked",
      +  "events_checked",
      +  "approval_events_status",
      +  "binding_status",
      +  "backend",
      +  "audit_anchor",
      +  "chain_status"
      +]
  2. Changed9 schema fields changedv0.5.0
    • addedOutput schema / properties / attributed_rows
      Added value: +{
      +  "description": "How many rows have a signed principal naming an account: a non-empty value\nunder the `uid` or `token` scheme, which this build could read back as\nsomething the daemon itself could have written.\n\nOnly a finding when `chain_status` is `\"intact\"`. Past a detected break the\nwalk stopped checking, so those rows' principals are claims: some may be\nauthentic, since deleting or reordering a row breaks the link while leaving\nlater signatures valid, and this tool cannot say which.",
      +  "format": "uint64",
      +  "minimum": 0,
      +  "type": [
      +    "integer",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / chain_status
      Added value: +{
      +  "description": "The transaction chain's own verdict: `\"intact\"`, `\"broken\"` or\n`\"cannot_verify\"`.\n\nReported separately because `status` is the worst of three checks, so a\nbroken *approval-event* chain sets `status` to `\"broken\"` while this stays\n`\"intact\"`. Read this one, not `status`, to decide whether the attribution\ncounts below are findings or claims: without it an agent had no way to\nrecover the chain verdict and would discard sound attribution.",
      +  "type": "string"
      +}
    • addedOutput schema / properties / rows_censused
      Added value: +{
      +  "description": "How many rows were censused for attribution: every row read, whether or\nnot it verified.\n\n`null` when the store could not be read at all, along with every count\nbelow, so a database nobody could open never reads as one where nothing was\nfound. A readable but empty store reports `0`.\n\nWhen `chain_status` is not `\"intact\"` this can exceed `rows_checked`, and\nthe difference is the part of the trail that was counted but not proven.",
      +  "format": "uint64",
      +  "minimum": 0,
      +  "type": [
      +    "integer",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows_naming_no_account
      Added value: +{
      +  "description": "How many rows name no account, for any reason. The complement of\n`attributed_rows` over `rows_censused`, provided so a reader does not have\nto add the three reasons and risk missing one.",
      +  "format": "uint64",
      +  "minimum": 0,
      +  "type": [
      +    "integer",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows_unattested
      Added value: +{
      +  "description": "How many rows have no principal any signature vouches for: the column is\npopulated on an encoding that does not sign it, or holds a value this build\ncannot read back as one the daemon could have written, or the row declares\nan encoding this build does not know.\n\nThis build writes none of those. The first two are out-of-band writes to\ninvestigate; the third means a newer SysKnife wrote the rows and the fix is\nto verify with a build at least that new.",
      +  "format": "uint64",
      +  "minimum": 0,
      +  "type": [
      +    "integer",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / rows_without_principal
      Added value: +{
      +  "description": "How many rows carry no principal the signature covers, normally because\nthey were signed before the column existed.\n\nReported next to `unattributed_rows` because zero attribution failures\nover a pre-v3 database would otherwise read as full attribution. The two\nhave different remedies: this one cannot be fixed, since backfilling a\nprincipal would rewrite the bytes the signature covers.",
      +  "format": "uint64",
      +  "minimum": 0,
      +  "type": [
      +    "integer",
      +    "null"
      +  ]
      +}
    • changedOutput schema / properties / unattributed_rows / description
      Previous value: -"How many verified rows record that the daemon could not name the caller.\n\n`status: \"intact\"` with a non-zero count here means the chain is sound and\nthe attribution is not: report both, never the first alone."New value: +"How many rows record that the daemon could not name the caller.\n\n`chain_status: \"intact\"` with a non-zero count here means the chain is sound\nand the attribution is not: report both, never the first alone.\n\nSince 0.4.0 this counts only rows whose `chain_version = 3` principal is\nsigned as `none:unattributed`. 0.3.0 matched the column on any encoding,\nwhich meant an unsigned column could land here; such rows are now\n`rows_unattested`."
    • changedOutput schema / properties / unattributed_rows / type
      Previous value: -"integer"New value: +[
      +  "integer",
      +  "null"
      +]
    • changedOutput schema / required
      Previous value: -[
      -  "status",
      -  "rows_checked",
      -  "events_checked",
      -  "approval_events_status",
      -  "binding_status",
      -  "backend",
      -  "unattributed_rows"
      -]New value: +[
      +  "status",
      +  "rows_checked",
      +  "events_checked",
      +  "approval_events_status",
      +  "binding_status",
      +  "backend",
      +  "chain_status"
      +]
  3. Changed3 schema fields changedv0.1.6
    • addedOutput schema / properties / daemon_socket_caveat
      Added value: +{
      +  "description": "Set when `SYSKNIFE_SOCKET` names a daemon that may not live on this\nmachine, because verification reads a local store while every other tool\ntravels over that socket. `None` for the local-daemon case.",
      +  "type": [
      +    "string",
      +    "null"
      +  ]
      +}
    • addedOutput schema / properties / unattributed_rows
      Added value: +{
      +  "description": "How many verified rows record that the daemon could not name the caller.\n\n`status: \"intact\"` with a non-zero count here means the chain is sound and\nthe attribution is not: report both, never the first alone.",
      +  "format": "uint64",
      +  "minimum": 0,
      +  "type": "integer"
      +}
    • changedOutput schema / required
      Previous value: -[
      -  "status",
      -  "rows_checked",
      -  "events_checked",
      -  "approval_events_status",
      -  "binding_status",
      -  "backend"
      -]New value: +[
      +  "status",
      +  "rows_checked",
      +  "events_checked",
      +  "approval_events_status",
      +  "binding_status",
      +  "backend",
      +  "unattributed_rows"
      +]
  4. Addedv0.1.2

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden, and it does well: it declares the tool read-only, lists the return fields, and discloses a subtle interpretation caveat (intact is not the same as complete attribution). It doesn't mention any access requirements (e.g., reading protected audit logs may need elevated privileges) or cost/latency, which keeps it below a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is front-loaded with the core action and return values, and each sentence contributes something (mechanism, outputs, interpretation guidance, safety note). The middle sentence about rows_censused and attributed_rows is dense but justified because it prevents misreading the status.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Although an output schema exists, the description summarizes the return values and adds the crucial interpretation warning that structured fields alone would not convey. Combined with the read-only safety statement for a zero-param tool, an agent has everything needed to call and interpret it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so the baseline is 4. The schema is empty and the description adds that no preparatory call is needed, which is the relevant semantic context for a parameterless tool.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names the specific mechanism (Ed25519-signed tamper-evident hash chain) and resource (audit log), and states the returned status values (intact/broken/cannot_verify). This clearly separates it from siblings like sysknife_get_audit_rules, which read rules rather than verify integrity.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly tells the agent that no prior sysknife_plan is required, which is meaningful guidance given the plan/execute workflow among siblings. It stops short of naming alternatives or stating when verification should be skipped, so it falls short of a full when/when-not treatment.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.