Real-Time Trust Alerts: How to React in Milliseconds When an Agent Fails a Check
Published May 28, 2026
An agent fails a trust check. Your system needs to know in milliseconds, not minutes.
That sentence is the entire reason webhooks exist as a primitive — and the reason "we'll poll the score endpoint every five minutes" is not an acceptable answer for anything that can place a trade, write to a patient record, or talk to a customer with a credit card in their hand.
This is the developer's view of how VeriSwarm pushes trust signals out of Gate and into your stack, what's actually in the envelope, and how to wire up a consumer that survives retries, replays, and the worst day your endpoint is going to have.
Polling loses the race the moment latency matters
The published benchmarks for AI incident operations in 2026 are not flattering for human-paced response. The current industry-tracked average mean-time-to-resolution for tier-one AI incidents sits between four and eight hours without automation; with AI-assisted response and push-based alerting, the same workload compresses to fifteen to forty-five minutes, and the experimental ceiling shown in published case studies pushes individual incident response from forty-five minutes down to under thirty seconds (Digital Applied, "AI Incident Metrics: MTTR / MTTD Framework Agentic 2026").
The gap between those numbers is the gap between "we found out" and "we did something." For a misbehaving agent, every minute on the wrong side of that gap is another batch of tool calls, another customer message, another row in the audit log that someone is going to have to explain.
EU AI Act Article 14 makes the structural version of this point explicitly. It requires that a designated person "be able to halt the AI system's operation in real time, preventing further AI-driven decisions," and that the mechanism be "accessible…without requiring manager approval or technical workaround" (ISMS.online, "Why Article 14 Demands Real Human Oversight"). A scheduled poll against a scoring endpoint is the technical workaround. It also fails the spirit of "real time" the moment the polling interval crosses thirty seconds, which is to say almost always.
The webhook is the primitive Article 14 has been waiting for. It's how the signal that triggered a deny decision in Gate becomes a revoked token in your authorization layer in the same network round-trip.
What VeriSwarm actually pushes today
The load-bearing event in the current shipping dispatcher is decision.checked. Every call into the trust-decision endpoint — the one your agent or your authorization layer hits to ask "am I allowed to do this thing right now" — emits a webhook against every active subscription on the tenant. There is no polling involved on your side; the decision lands at your endpoint at the moment it's made.
The companion event is provider.report.ingested, fired when a cross-provider risk report arrives via the shared-reputation surface. That one matters for inventory and risk dashboards more than for incident response.
Each delivery is a POST of a JSON body whose top-level shape is stable across event types:
{
"event_type": "decision.checked",
"occurred_at": "2026-05-28T14:32:11.482000+00:00",
"data": {
"tenant_id": "tnt_...",
"agent_id": "agt_8c4f...",
"action_type": "external_tool_usage",
"decision": "deny",
"reason_code": "agent_killed",
"policy_tier": "tier_x",
"risk_score": null,
"score_snapshot_id": "ss_8e2a..."
}
}
Three response headers carry the security context:
x-veriswarm-event— the event type, repeated for routing conveniencex-veriswarm-timestamp— a UTC Unix timestamp, integer secondsx-veriswarm-signature— the HMAC-SHA256 of the timestamp and the raw body, prefixedv1=
The dispatcher accepts subscriptions for a broader taxonomy — agent lifecycle, tier changes, guard findings, vault chain integrity — and those event names are validated against a closed allow-list when you create a subscription. The emissions on those names are being wired through the codebase in stages; the decision-checked envelope is the primary surface that fires in production today, and it carries enough signal in decision, reason_code, and policy_tier to drive every incident-response handler that matters right now.
The signature contract, and why timestamp-in-the-HMAC is non-optional
Recent industry guidance on webhook security in 2026 has converged on a small number of rules that distinguish a real signing scheme from a decorative one. The summary in FreelyIT's API Security Best Practices roundup puts the load-bearing rule plainly: the timestamp must be part of the HMAC calculation, because otherwise an attacker who captures a valid signed body can replay it later under a fresh timestamp and the signature still verifies.
VeriSwarm signs f"{timestamp}.{body}" — the timestamp is inside the bytes the HMAC sees. A consumer that verifies signatures correctly does three things:
- Reads the
x-veriswarm-timestampheader and rejects anything older than five minutes. This caps the replay window to something the laws of physics won't let an attacker exploit casually. - Recomputes
HMAC-SHA256(secret, f"{timestamp}.{raw_body}")and compares it to the stripped hex fromx-veriswarm-signatureusing a constant-time comparison. String equality leaks timing. - Treats a failed signature as an authentication failure, not a parse error. Don't process the body, don't queue it for retry, don't log the payload — drop it.
The Python skeleton is short enough to keep on a wiki page:
import hmac, hashlib, time, os
from fastapi import Request, HTTPException
WEBHOOK_SECRET = os.environ["VERISWARM_WEBHOOK_SECRET"]
MAX_AGE_SECONDS = 300
async def veriswarm_webhook(request: Request):
body = await request.body()
timestamp = request.headers.get("x-veriswarm-timestamp", "")
sent_sig = request.headers.get("x-veriswarm-signature", "")
if not timestamp or not sent_sig.startswith("v1="):
raise HTTPException(401, "missing_signature")
if abs(time.time() - int(timestamp)) > MAX_AGE_SECONDS:
raise HTTPException(401, "timestamp_too_old")
material = f"{timestamp}.{body.decode('utf-8')}".encode("utf-8")
expected = hmac.new(WEBHOOK_SECRET.encode("utf-8"),
material, hashlib.sha256).hexdigest()
if not hmac.compare_digest(f"v1={expected}", sent_sig):
raise HTTPException(401, "bad_signature")
return handle(request.headers["x-veriswarm-event"],
await request.json())
The Node equivalent is the same shape: a crypto.createHmac("sha256", secret).update(...).digest("hex") followed by crypto.timingSafeEqual. The SDK ships a verification helper so most consumers don't write this by hand, but understanding what it does is the difference between a working endpoint and a CISO question you can't answer.
What happens when your endpoint has a bad day
Every webhook receiver eventually returns a 503, a 504, or an unfortunate stack trace. The dispatcher's behavior on the failure path is the part developers tend to under-specify until production teaches them otherwise. VeriSwarm's contract is short:
- A delivery is considered successful only on a 2xx status. Anything else, including a redirect, is a failure. Redirects are deliberately not followed; an attacker who can poison DNS or an internal config should not be able to pivot a webhook callback into a request against an internal service.
- A failed delivery is retried up to three times with exponential backoff: thirty seconds, two minutes, five minutes. Total time-on-the-clock from first attempt to giving up is roughly seven and a half minutes.
- Per-attempt request timeout is two and a half seconds. Long-running webhook handlers are an antipattern — acknowledge fast, queue the work behind the response.
- After ten consecutive deliveries land in the
exhaustedstate on the same endpoint, the dispatcher trips a circuit breaker and disables the endpoint until an operator re-enables it. This is the system refusing to keep paging the same dead URL indefinitely.
The implication for consumer design is uncomfortable but clear: your handler will be called more than once for the same event. Network timeouts get retried. Mid-deploy 502s get retried. The same deny decision may arrive twice because the first 200-OK didn't make it back across a flaky link. Idempotency is your problem, not ours; the cheap fix is to track the trio of agent_id, action_type, and score_snapshot_id from the payload and short-circuit on a repeat.
The two-minute incident-response wire-up
Of the fields in the decision.checked envelope, three are load-bearing for an incident playbook: decision, reason_code, and policy_tier. The pattern is the same in every framework: subscribe, verify, dispatch, act.
def handle(event_type: str, payload: dict) -> None:
if event_type != "decision.checked":
return
data = payload["data"]
agent_id = data["agent_id"]
decision = data["decision"]
reason = data.get("reason_code")
if decision == "deny" and reason == "agent_killed":
revoke_all_sessions(agent_id)
page_oncall(f"Gate denying {agent_id}: kill switch active")
elif decision == "deny":
revoke_all_sessions(agent_id)
elif decision == "review":
require_human_approval(agent_id, data["action_type"])
# decision == "allow" — let the agent proceed
Four branches, fewer than twenty lines, and the latency from "Gate decides something is wrong" to "your authorization layer stops trusting the agent" is whatever the round-trip to your endpoint plus the work in those branches costs you. The dispatcher does its part in under a second per attempt against a healthy endpoint; the rest is yours.
A kill switch hit in the console flips the agent's killed flag immediately and persists it. The next decision check against that agent comes back as decision: "deny", reason_code: "agent_killed", policy_tier: "tier_x" — which arrives at your endpoint as a webhook delivery in the same request cycle. Your authorization layer revokes; the agent's next tool call lands in your "denied" branch instead of execution. The kill becomes universal at network speed, not at polling-interval speed.
What to do next
The webhook configuration lives in the account console: pick the events you want, paste an HTTPS URL, copy the signing secret, save. The signing secret is shown once on creation, prefixed vwhsec_, and is the only piece of state your verification code needs.
The decision-checked payload reference and the JSON shapes are in the developer docs. The verification helpers ship in the Python and Node SDKs (github.com/veriswarm/veriswarm-sdk) — if you only take one thing from this post, take the one-line verify_webhook(...) call from the SDK and stop hand-rolling HMAC verification. Hand-rolled HMAC is how timing leaks ship.
The Gate free tier includes webhook delivery. There is no plan-gated paywall in front of the primitive that turns a detection into a response. That part of the stack should not be optional.
Sources
- Digital Applied — AI Incident Metrics: MTTR / MTTD Framework Agentic 2026 — 4-8h baseline MTTR, 15-45min with AI-assist, sub-30s experimental ceiling
- ISMS.online — Why Article 14 Demands Real Human Oversight — Article 14 real-time intervention requirement
- FreelyIT — API Security Best Practices 2026 — Timestamp-inside-HMAC as the load-bearing replay defense
Related Reading
- Pillar: PII Protection for AI Agents — the full guide to runtime tokenization at the agent-tool boundary, the Presidio + Guard Proxy architecture, the GDPR Article 4(5) caveat, and how Vault records every PII touch.