Events and alerts
The closed event vocabulary, the heartbeat, and the alert catalogue that is the platform's only control surface.
Source: verifi-engine-contract/src/event.ts.
zEvent
export const zEvent = z.object({
id: z.string().min(1),
runId: z.string().min(1),
/** Threaded from the first byte. Without it an event cannot be joined to its run. */
correlationId: z.string().min(1),
type: zEventType,
agentId: z.string().optional(),
severity: z.enum(['debug', 'info', 'warn', 'error']),
ts: zISO,
payload: z.record(z.string(), z.unknown()).default({}),
}).strict();The SDK fills runId, correlationId and ts for you; you supply type, severity and
payload.
zEventType
export const zEventType = z.enum([
'phase_start',
'phase_end',
'agent_spawn',
'agent_reap',
'tool_call',
'tool_result',
'finding',
'heartbeat',
'approval_request',
'approval_decision',
/** Replaced budget_warn/budget_exceeded when cost stopped being terminal. */
'threshold_breach',
/** A discovered host; outside the authorized apex it needs HITL confirm. */
'scope_expansion',
/** Declared coverage not achieved — a first-class run output. */
'coverage_gap',
'error',
'log',
// ── emitted by the harness SDK; added in contract 1.4.0 ──────────────────
/** A tool call that FAILED. Distinct from tool_result: it cost a round trip and returned nothing. */
'tool_error',
/** A slot switched to its fallback. Raised ONCE per slot per run. Also an alert key. */
'mcp.fallback_used',
/** A slot has nothing healthy behind it — a lost capability, not a slow one. */
'mcp.no_healthy_tool',
'model_call',
'model_error',
/** A model whose price is unknown, so its spend records as $0. Emitted once. */
'model_price_unknown',
]);Twenty-one members. Verified against contract 1.5.1.
mcp.fallback_used and mcp.no_healthy_tool are members of both zEventType and
zAlertKey. That is deliberate rather than accidental: the run timeline needs the event to
explain a gap, and an operator needs the alert. The same name in both places is what stops
the two describing the same thing differently.
The harness SDK does not enforce the enum — emitEvent's type is EventType | string,
so the members autocomplete and a novel string still compiles. Stay inside it for events
you emit yourself; that is what makes them legible to the console.
Two members are worth calling out for harness authors:
scope_expansion — you discovered a host outside the authorized apex. Emit it rather
than scanning it. security.scope_expansion_outside_apex is an urgent analyst alert.
coverage_gap — you did not achieve coverage you declared in declarePlan(). This is
a first-class run output, not an absence. A passed check is not a finding, and a check that
never ran is not a pass.
zHeartbeat
export const zHeartbeat = z.object({
runId: z.string().min(1),
agentId: z.string().min(1),
ts: zISO,
phase: z.string().min(1),
spend: zSpend,
/** Monotonic per run. The engine persists the last acked seq, so a re-dialed
* stream replays from that cursor and delivery is idempotent by (runId, seq). */
seq: z.number().int().nonnegative(),
}).strict();GrpcEngineChannel sends a keepalive heartbeat every keepaliveMs (default 25s)
automatically. You do not send these.
The alert catalogue
export const zAlertUrgency = z.enum(['info', 'digest', 'warn', 'urgent', 'critical']);
/** Analyst and ops are different people with different urgencies. */
export const zAlertAudience = z.enum(['analyst', 'ops', 'both']);zAlertKey is a closed enum rather than free-form strings, because alerting is the
platform's primary control surface — nothing is hard-capped except time, so an alert is the
only thing that stops a runaway. A typo'd alert key is a control that silently does not
exist.
ALERT_CATALOGUE maps every key to its default audience and urgency. The ones a harness or
MCP author will actually meet:
| Key | Audience | Urgency | Meaning |
|---|---|---|---|
run.findings_ready | analyst | warn | A run produced findings to review. |
run.completed_zero_findings | both | warn | A clean run is either a healthy target or a broken scanner. |
run.coverage_gap | analyst | warn | Declared coverage was not achieved. |
run.failed | both | warn | |
run.timed_out | both | warn | The one terminal bound. |
run.provision_failed | ops | warn | |
run.queued_too_long | ops | warn | |
hitl.pending | analyst | urgent | A destructive action is waiting on approval. |
hitl.escalated | analyst | urgent | |
hitl.timeout_proceeded / hitl.timeout_skipped | analyst | warn | |
threshold.run_cost · run_tokens · run_tool_calls · run_evidence_bytes · run_duration_warning | ops | warn | The cap replacement. Alerts; stops nothing. |
threshold.assessment_cost | ops | warn | |
threshold.platform_daily_burn | ops | urgent | |
quota.tenant_approaching | ops | warn | |
quota.tenant_exceeded | ops | urgent | A commercial event, not a technical one — nothing is blocked. |
mcp.fallback_used | ops | warn | Silent failover is how a degraded primary hides for months. |
mcp.no_healthy_tool | both | urgent | A slot has nothing behind it. |
mcp.probe_failed | ops | warn | |
model.probe_failed | ops | warn | |
kb.zero_chunks | ops | warn | Answers HTTP, retrieves nothing. |
kb.proposals_pending | analyst | digest | |
security.egress_denied_internal | ops | critical | A container reached for the instance metadata service or our VPC. Always an incident. |
security.scope_expansion_outside_apex | analyst | urgent | |
security.credential_probe_failed | ops | warn | |
schedule.auto_paused | both | urgent | A paused schedule is silently not testing. |
schedule.occurrence_skipped / occurrence_missed / service_drift | analyst / ops | digest | |
residency.probe_failed | ops | warn | |
residency.unreachable | both | urgent | The client's dashboard is showing an error right now. |
residency.publish_refused | analyst | urgent | |
residency.migration_verify_failed | ops | urgent | |
residency.purge_awaiting_confirm | ops | info | |
engine.component_unavailable | ops | critical | |
engine.lease_adoption_storm | ops | urgent | |
engine.boot_orphans_found | ops | warn | |
engine.teardown_leak | ops | urgent | A teardown report's leaked list must be empty. |
export const zAlert = z.object({
key: zAlertKey,
urgency: zAlertUrgency,
audience: zAlertAudience,
raisedAt: zISO,
/** What the alert is about: runId, tenantId, scheduleId, mcpId… */
subject: z.record(z.string(), z.string()).default({}),
message: z.string().min(1),
acknowledgedAt: zISO.optional(),
acknowledgedBy: z.string().optional(),
}).strict();Harnesses do not raise alerts directly. You emit events; the engine derives alerts. The
catalogue is here because knowing which of your events becomes an urgent page for a human
should change how carefully you emit it.
Threshold breaches
export const zThresholdBreach = z.object({
metric: z.enum(['cost_usd', 'tokens', 'tool_calls', 'evidence_bytes']),
threshold: z.number(),
actual: z.number(),
alertedAt: zISO,
});Each level fires at most once per run. See The budget ledger.
security.egress_denied_internal
The only critical alert a harness can trigger by accident. A harness container that
reaches for 169.254.169.254 (cloud instance metadata) or an internal VPC address raises
it, and it is always treated as an incident.
You will not trigger it by accident if you only ever reach the network through
ctx.mcp() — which is another reason there is no raw HTTP client on the context.