Bands, entity alerts and dispositions
Tessera turns per-entity scores into alert episodes you can triage, and
measures how often those alerts were right. Everything is configured per
deployment slot as data and served from the alerting API (alerts tag);
measured precision is served under monitors.
Alert rules
A rule is a predicate over the scored entity state — the same predicate
language the platform uses everywhere (predicate.json): a tree of
all / any / not nodes over leaves that select a score, a propensity,
behavior surprise or the segment. Alert rules can also select:
{"kind": "band", "label": …}— the band of a bound label (see Bands), e.g."op": "eq", "value": "critical"; a label without a binding never matches;{"kind": "quiet_state", "field": "silence_seconds" | "silence_threshold_crossed"}— the quiet-state fields of a slot with observation time enabled, in seconds; a write without them never matches.
These two selects exist only for alert rules: audiences and suppression sets
reject them with invalid_argument.
{
"name": "lateral movement",
"predicate": {
"all": [
{ "select": { "kind": "score", "label": "compromise_7d" }, "op": "gte", "value": 0.9 },
{ "select": { "kind": "behavior_surprise" }, "op": "gte", "value": 0.95 }
]
},
"cooldown_seconds": 3600,
"auto_resolve_after_seconds": 86400
}
POST /slots/{slot}/alert-rulescreates a rule (adminscope). Unknown fields, operators or selects are rejected (invalid_argument); the predicate is validated by the platform's single predicate validator.- A
scoreorbandselect must name a label the slot's active bundle scores: a band needs a binary label, and a multiclass label needs one of itsclassvalues. A rule that could never match is rejected withinvalid_argumentnaming the label and the bundle, on create and when aPATCHchanges the predicate. - A slot holds at most 64 active rules (configuration data). Creating or
re-activating a rule beyond the cap fails with
failed_precondition(reasonalert_rule_limit_reached); paused rules do not count. PATCHupdates any field (label_bridge: nullremoves the bridge); pausing a rule stops matching within 30 seconds.DELETEretires the rule while keeping its episodes, dispositions and audit history.
The packaged quiet-state rule
POST /slots/{slot}/alert-rules:install-went-quiet (admin scope) installs
the packaged entity.went_quiet rule on a slot. It is an ordinary rule — same
episodes, cooldown, sweep, dispositions and measured precision — whose
episodes publish entity.went_quiet instead of alert.raised:
{ "threshold_seconds": 1209600, "cooldown_seconds": 604800 }
All fields are optional. Without threshold_seconds the rule matches any
configured silence-threshold crossing; with it, crossings of at least that
many seconds. It needs observation time with silence
thresholds on the slot: without quiet-state fields the rule simply never
matches. A slot holds at most one packaged rule (a second install answers
409 already_exists; PATCH or delete the existing one), and it takes an
active-rule seat like any rule.
Episodes
Rules are evaluated on every state write — event-driven and scheduled re-scores alike — and a match opens an episode:
- at most one open episode per rule and entity; further matches only
advance
last_matched_at(recorded at one-second granularity); - the episode keeps an immutable opening snapshot: scores, bands (for
bound labels), the behavioral evidence that explains the score, and the
score's provenance (
freshness); - after an episode closes, a new one opens only once
cooldown_secondshave elapsed since the previous episode opened; - an episode unmatched for
auto_resolve_after_secondsis auto-resolved (the resolution sweep runs every 30 seconds).
GET /slots/{slot}/alert-episodes?status=open|resolved&rule_id= lists
episodes newest first; GET …/alert-episodes/{episode_id} returns one.
Deleting an entity's data (a data-subject deletion) also removes its alert episodes, their dispositions and its alert deliveries.
Webhooks
Each transition publishes exactly one webhook through your webhook
endpoints (signed with Tessera-Signature — verify it exactly as for every
other event type):
| Event | When |
|---|---|
alert.raised | an episode opens |
alert.resolved | an episode closes (auto_resolved or dispositioned) |
score.band_changed | a bound label's band changes between two writes |
entity.went_quiet | the packaged quiet-state rule opens an episode |
Every alert payload carries the opening snapshot, so a receiver never needs
to call back for context — up to the 64 KiB envelope bound: payloads over
64 KiB omit signals, then evidence/frame; fetch the episode via REST
(GET …/alert-episodes/{episode_id}) for the full snapshot. Envelopes are delivered at least once: deduplicate
on the envelope id (a re-delivery of the same transition has the same id).
Alert webhooks pass the platform's delivery suppression filter immediately
before they are sent: a suppressed entity's alert never leaves the platform,
while its episode stays available for triage. A transition whose delivery
could not be decided or sent is retried by the sweep (with a growing delay, up to
15 minutes) with the same envelope id, and never delays alerts behind it.
Envelopes are not ordered relative to each other, and a transition can arrive
late: when a batch refresh's alert evaluation fails, it is evaluated again
later exactly as it would have been then (the refresh's instant, the refresh's
rules, the scores as written), so a score.band_changed can arrive after a
later band change of the same entity. To apply band changes in order, order
them per entity by data.freshness.event_cursor (<microseconds>#<event id>:
compare the number before # first), then by data.freshness.computed_at;
a change older than the one you applied last is history, not the current band.
Such a late evaluation applies the rules as they were when the refresh ran,
except that a rule you paused or deleted since then, and a label you unbound,
is skipped: neither raises a late alert.raised or score.band_changed. If a
refresh's evaluation could not be recorded at all, the next refresh evaluates
what that refresh wrote exactly as that refresh would have (its rules and
bindings, its instant); for an entity whose score a later write already
replaced, only the band change is recorded (the newer state wins). A
refresh's rules are kept for 35 days after anything last needed them (every
later refresh that still has its writes to evaluate needs them again). Only
when no refresh needed them for 35 days, because for that long the slot had
no rules and no band bindings, no refresh ran, its active bundle was not live
(shadow), or every refresh failed at its start, are your current rules used
instead (their current versions, and without a band view for an entity whose
band did not change); that differs from the refresh's own rules only if you
changed the slot's rules or bindings meanwhile.
Dispositions
Record what an alert turned out to be:
{ "disposition": "true_positive", "reason": "confirmed by incident response" }
POST /slots/{slot}/alert-episodes/{episode_id}:disposition accepts
true_positive, false_positive, benign or duplicate. Each judgment is
appended (the latest wins), written together with its audit record in one
transaction, and closes an open episode. Send an Idempotency-Key to make
retries safe.
Feeding labels back. A rule may carry a label_bridge naming a binary,
event-sourced label of the slot's entity type. Then every true_positive /
false_positive disposition becomes one label event (value true / false)
observed at the moment of the disposition — never backdated to when the
alert opened — and is usable by the next dataset build. benign and
duplicate emit nothing. The label events arrive through the
platform-managed alerts:dispositions stream source; if another source of
that name exists in the environment, or the platform source was disabled,
bridged dispositions fail with failed_precondition (reason
label_bridge_source_conflict or label_bridge_source_inactive) until it is
renamed or re-enabled — or the rule's label_bridge is removed. Label events
are published right after the judgment commits and retried automatically (with
a growing delay) if the platform cannot publish one at that moment; a bridge
the label router refuses for an event is skipped and logged — the judgment
itself always lands.
Measured precision
GET /slots/{slot}/monitors/alert-precision?rule_id=&from=&to= returns, per
rule, the episodes opened in the window, how many were dispositioned, the
true/false-positive counts and
precision = true_positive / (true_positive + false_positive)
Undispositioned, benign and duplicate episodes count in volume but never
in the denominator; precision is null until something was judged. The
unfiltered listing covers the slot's live rules; a deleted rule's precision
stays readable with rule_id=.
GET /slots/{slot}/monitors/alert-precision/weekly?rule_id=&from=&to= serves
the realized alert-precision series: one point per rule and ISO week
(Monday, UTC) over the episodes opened that week, with the same counts and
precision, newest week first. Points are recomputed every hour over the
trailing 12 weeks, so a disposition shows up in its week's point within about
an hour — including a late judgment of an older episode.
Bands
Bands bind, per label, a fingerprinted banding policy — the same policy object your evaluation measured:
{
"bindings": [
{
"label": "compromise_7d",
"policy": {
"bands": [
{ "band": "critical", "min_score": 0.9 },
{ "band": "elevated", "min_score": 0.5 }
],
"list_size": 100,
"alert_budget": 25
},
"policy_fingerprint": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
]
}
The policy_fingerprint in this example is a placeholder shaped like a real
one (64 lowercase hex characters) — use the fingerprint of your own policy, the
one recorded with its evaluation.
PUT /slots/{slot}/bands replaces the slot's bindings (an empty list clears
them); each policy_fingerprint must equal the fingerprint the platform
computes for its policy, or the binding is rejected. Bands are derived from
the bound policy whenever they are emitted — never stored with the entity
state — and slots without bindings carry no band fields at all. Episode
snapshots, alert payloads and score.band_changed carry bands; the existing
state reads, exports and score.updated payloads are unchanged. Read bands
for those surfaces through two additive operations that apply the same bound
policy:
GET /slots/{slot}/entities/{entity_type}/{entity_id}/bandsreturns the bands of the entity's served state (readscope;bandsis{}when the slot has no binding);POST /slots/{slot}/bands:derivederives bands for score sets you already hold — thescore__<label>columns of an export row, or thescoresof ascore.updatedpayload:
{ "items": [{ "key": "shp-00042", "scores": { "compromise_7d": 0.93 } }] }
Availability
Rule evaluation and band derivation use the platform's shared predicate
evaluator and banding implementation. On a deployment where either is
unavailable, the affected writes answer failed_precondition with reason
predicate_evaluator_unavailable or banding_unavailable — nothing is
stored unvalidated, and reads, episodes, dispositions and measured
precision keep working.