Atmosphere Mail SML Rules#
SML rules consumed by the Osprey worker to evaluate relay events and
apply labels to senders. Entry point: main.sml. Per-rule logic under
rules/*.sml, keyed on EventType. The action-scoped files under
actions/*.sml exist so the Osprey UI's validator discovers the action
names — the real rule logic lives in rules/.
Label declarations live in ../config/labels.yaml.
Layout#
main.sml entry — Import models, Require every rule file
models/relay.sml entity + field definitions the rules read
rules/<rule>.sml one rule per file, keyed on EventType
actions/<action>.sml one-liners per action so the UI enumerates them
Shadow mode (observe before enforce)#
High-impact rules (anything that auto-suspends, skips warming, or halves a member's rate) should land in shadow mode first. Run observation-only for a bake-in period; promote to enforce once the observed verdicts look right.
The convention is purely at the label level:
- A rule in shadow mode emits
shadow:<label>instead of its real label. Example: a new suspension rule appliesshadow:auto_suspendedinstead ofauto_suspended. - The relay's
policyFromLabels(internal/relay/ospreyenforce.go) ignores anything with theshadow:prefix — seerelay.IsShadowLabel. A shadow label therefore has no effect on sending policy. - Shadow verdicts still land in
relay_events.labels_appliedand surface at/admin/shadow-verdictsso ops can audit what the rule would have done. - When the observed behavior looks right, rename the label in the
rule from
shadow:<label>to<label>and redeploy. No engine changes, no config flags.
Authoring a shadow rule#
In the rule file, replace the enforcing label add with the shadow variant. For example, to shadow a new bounce-rate suspension rule:
# rules/bounce_aggressive.sml (shadow)
when(EventType == 'bounce_received') {
if (SenderDID.bounce_rate_24h > 0.10) {
LabelAdd(SenderDID, 'shadow:auto_suspended')
}
}
Declare the shadow label in ../config/labels.yaml alongside its
real counterpart — label declarations are checked at validate time.
Promote by deleting the shadow: prefix in the LabelAdd call and
in the labels.yaml declaration. A single-line diff keeps the
before/after easy to eyeball.
Auditing shadow verdicts#
/admin/shadow-verdicts(admin UI, Tailscale-only) — filtered view ofrelay_eventsshowing only events whoselabels_appliedcontains ashadow:label.SELECT * FROM relay_events WHERE labels_applied LIKE '%/shadow:%'in/var/lib/atmos-relay/relay.sqlitefor deeper queries.relay.IsShadowLabel(label)— Go-side helper used in tests and when building admin surfaces.
When not to shadow#
- Label changes that are pure refactors (renaming, splitting existing rules) don't need shadow mode.
- Rules that only add observational labels (no policy effect) don't need shadow mode — those are already harmless.
- Anti-abuse rules where delaying enforcement costs more than a false positive — judgment call; flag in the PR description.