Skip to main content

Verdicts and audit

Every check produces a verdict, and every verdict is recorded. That record is the reason monitor mode is worth anything: it is how you find out what enforcement would have done before you turn it on, and how you answer "why was this refused" afterwards.

Reading the feed​

One agent:

platformctl agents security verdicts research-buddy --limit 20
TIME DIRECTION DECISION TIER CATEGORIES LATENCY ID
2026-09-16 11:31:09 output allow pattern - 22ms gv_01J9X1M4S6ZQ8KC4
2026-09-16 11:31:07 tool_result block classifier - 41ms gv_01J9X1M4S6ZQ8KC2
2026-09-16 11:31:05 input mask semantic - 238ms gv_01J9X1M4S6ZQ8KC0
2026-09-16 11:18:02 input flag semantic jailbreak 511ms gv_01J9X1M4S6ZQ8K9F

Narrow it three ways:

# Only refusals, this week.
platformctl agents security verdicts research-buddy --decision block --since 7d

# Only masking, the last half hour, a short page.
platformctl agents security verdicts research-buddy --decision mask --since 30m --limit 10

# The whole record, for a script.
platformctl agents security verdicts research-buddy --limit 1000 -o json

--decision takes allow, flag, mask or block. --since takes a span written the way you would say it - 30m, 24h, 7d. --limit is 1–1000 and is always sent, because a feed with no bound prints ten thousand rows the first time an agent gets busy.

Across the whole project, how much is happening rather than what happened:

platformctl guard stats --window 24h

Read the two together: stats answers "how much", verdicts answers "what".

What a verdict contains​

FieldWhat it is
idThe verdict id, gv_ followed by a sortable identifier. The one thing to quote.
directioninput, output, tool_args or tool_result. Read this first - tool_result means something your agent fetched was the problem.
decisionallow, mask, flag or block.
reasonOne customer-safe sentence. No raw content, no detected value, nothing about how the check works. Safe to show a user, which is what include_reason does.
riskscore 0–1, severity (safe, controversial, unsafe), and the safety categories that fired. Populated by the semantic tier.
tier_reachedThe last tier that ran.
short_circuitedtrue when a decision stopped the run early.
scannersOne entry per check that ran: name, tier, decision, latency_ms, error, plus for pii and secrets an entities array of {type, count}.
session_hashA hash of the session id - not the session id. Groups a conversation's verdicts without naming it.
request_idThe request id you passed in, echoed back.
policysource (which layer supplied the policy), version and mode.
modelPresent only when the semantic tier ran: the profile and the tokens it used.
latency_msThe whole check, end to end.
created_atWhen.

What it deliberately does not contain​

  • The text. Not the message, not the reply, not the tool result, not a truncated excerpt.
  • Any detected value. The pii entry says [{"type":"EMAIL_ADDRESS","count":1}]. It never says which address.
  • The session id. Only a hash of it.
  • Anything about the machinery. No engine names, no model identifiers beyond the profile name you chose, no internal addresses.

This is a design decision and not a limitation to work around. A store of everything your agents refused is a store of exactly the content somebody would most like to steal, and it would make the feature impossible to use in any regulated setting. What you get instead is a feed that is safe to read, safe to export, safe to paste into a ticket, and safe to hand to a vendor.

The cost is that you cannot see the blocked message here. If you need to, that is what session_hash and request_id are for: they line a verdict up against your own application logs, where your own retention and your own rules apply. You decide whether to keep the content; the platform has decided not to.

Log lines the platform writes about a check carry the same discipline: the text's length and a short hash of it, never the text.

The audit trail​

A block and a flag also write to the project's audit log, alongside membership changes, credential issuance and secret reveals. So does a policy change.

ActionWritten when
agent.guard.blockA request was refused.
agent.guard.flagA concern was recorded and the request went through - including every downgraded block in monitor mode.
agent.guard.policy.changeSomebody changed a guardrail policy.
platformctl audit --limit 20

Two things about these entries:

  • target is agent/<name>, so filtering the audit log by one agent gets you its refusals alongside every other administrative event that touched it - which is usually the question you actually have ("what changed about this agent, and when did it start refusing things").
  • A block or flag entry has no human actor. Nobody chose it; a check did. A policy.change entry does have one, and that is the entry that answers "who turned this on".

detail carries the verdict id, so an audit entry is always one lookup away from the full verdict - and never carries content, for the same reason the verdict does not.

Retention​

logging.retention_days on the policy sets how long a verdict is kept. It defaults to 30 days and accepts 1 to 365.

platformctl agents security set research-buddy --retention-days 90
# Or as the project-wide default, so every agent inherits it.
platformctl guard policy set --retention-days 90

logging.store_verdicts: false turns recording off entirely. Think hard before using it: you lose monitor mode, you lose the feed, and you lose the ability to answer "why was this refused" at all. The reason to keep it on is that verdicts carry no content, so there is very little to be gained by not having them.

Audit entries are not governed by retention_days. The project audit log is append-only and permanent, like every other entry in it. A verdict expires; the audit record that a refusal happened does not.

Using a verdict id with support​

Turn on include_verdict_id and a refused user is handed something quotable:

platformctl agents security set research-buddy --refusal-include-verdict-id
Sorry, I can't help with that. Contact support@acme.example if you think this is a mistake. [gv_01J9X1M4S6ZQ8KC2]

That is the right thing to expose, because it reveals nothing on its own and your desk can resolve it in one command:

platformctl agents security verdicts research-buddy --decision block --since 24h -o json

Then find the id. Verdict ids sort by time, so the id itself tells you roughly when to look.

What the desk learns from a verdict, and can act on:

  • direction - was this the user's message, the agent's reply, or something the agent fetched? A tool_result block is not the user's fault at all.
  • reason - one sentence saying what kind of thing fired.
  • scanners - which check, at which threshold, and how long it took.
  • policy.version and policy.source - which document was in force, and which layer supplied it. This is what answers "it worked last Tuesday": compare against the policy history.
  • request_id and session_hash - the keys into your own logs, if you keep the content.

A useful pattern for a support desk: paste the quoted id into a small internal tool that runs the command above and prints direction, reason, the fired check and the policy version. Four facts, no content, and enough to decide whether the policy needs changing or the user needs a different route.

If the answer is "the policy is wrong", the fix is a threshold or an action, and monitor mode plus guard scan is how to verify it before shipping:

platformctl guard scan --agent research-buddy --text "the message the user quoted to you"

Watching for trouble​

Three things worth an alert, all readable from the feed:

  • A tool_result block. Something one of your data sources is serving carries instructions aimed at your agent. That is an incident about the source, not about the agent.
  • A rising flag rate with error populated on a scanner. A check is failing or timing out, and fail: open is letting requests through unchecked. The reason says so: A security check could not complete, so the request was recorded and allowed.
  • A non-zero unknown count on a restore. Placeholders reached an end user, which looks like a bug in your agent. Usually the per-session cap, or a mapping reference that was not carried forward.

Next steps​