Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions skills/spam-risk-reviewer/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
---
name: spam-risk-reviewer
description: Judge one campaign send against sealed list hygiene and sender-auth posture, then emit a bounded send_risk_verdict for downstream governed lanes.
runx:
category: ops
---

# Spam Risk Reviewer

`spam-risk-reviewer` decides whether a campaign send should be allowed to leave
the operator queue. It is a read-only judgment skill: it never sends mail,
mutates list state, mints authority, or executes a campaign send.

The default `spam_risk` runner is a graph with a thin `review` act. Its verdict
step is an agent-mediated judgment, so an unattended run stops at `needs_agent`
instead of fabricating a verdict. The bounded `dogfood` runner applies the same
checked contract deterministically for reproducible post-publish evidence.

The useful output is a single `send_risk_verdict`. A downstream `send-as` run
reads the verdict into its `preflight_required` and `blockers` slots; a non-clear
verdict prevents `send-as` from satisfying its preflight and routes the call to
the human approval lane. The `public_send` Effect stays `send-as`'s own, and
the actual delivery is that separate governed `send-as` run.

## What This Skill Does

The skill reads three pieces of evidence:

- `campaign_draft{from, subject, content_digest}`
- `list_metadata{size, bounce_rate, complaint_rate, freshness}`
- `sender_auth_posture{spf_pass, dkim_pass, dmarc_pass, warm_up_days}`

It verifies the sender has all three auth signals passing, checks the list
hygiene metrics against policy thresholds (default: bounce_rate <= 0.05,
complaint_rate <= 0.001, freshness <= 30 days idle), and inspects the
content digest for known risk flags. When the verdict is `pass`, the
verdict sets `preflight_clear: true` with an empty `blockers`. When the
verdict is `hold` or `block`, the verdict sets `preflight_clear: false`
and lists every blocker reason.

The skill never invents an authentication signal it cannot ground, never
clears preflight when any auth signal is missing or any policy threshold
is violated, and never executes the send.

## When To Use It

- An operator has a campaign draft and needs a receipt-backed spam-risk
decision before `send-as` is allowed to dispatch.
- A workflow needs to prove which auth signals and which hygiene metrics
justified clearing or holding the send.
- A run should separate judgment from action, so humans can review the
verdict before any mail is dispatched.

## When Not To Use It

- To actually send, queue, schedule, or otherwise move a campaign. Use a
downstream governed `send-as` run for that effect.
- To clear a send whose authentication signals are absent, missing, or
fabricated.
- To clear a send whose list violates the bounce, complaint, or freshness
threshold.
- To make up missing campaign fields, list metrics, or auth posture
signals.

## Procedure

1. Read `campaign_draft`, `list_metadata`, `sender_auth_posture` and reject
any missing or unclear top-level object.
2. Validate that `from` is a string, `subject` is a string, and
`content_digest` is a string.
3. Confirm `sender_auth_posture` for each of `spf_pass`, `dkim_pass`,
`dmarc_pass`, and `warm_up_days`. If any is missing or non-boolean
where boolean is required, set `preflight_clear: false` and add a
blocker reason naming the missing signal.
4. Confirm `list_metadata` for each of `size`, `bounce_rate`,
`complaint_rate`, and `freshness`. If any is missing, set
`preflight_clear: false` and add a blocker reason.
5. Inspect `content_digest` for known spam-risk patterns (e.g.
"free money", "click here now"). For each pattern, add a blocker
reason and set `risk_level` to `hold` (or `block` when combined with
auth failure).
6. Compute the verdict:
- `risk_level: pass` and `preflight_clear: true` if all auth signals
pass and all hygiene metrics are within policy and no content risk
flags were found.
- `risk_level: hold` and `preflight_clear: false` if any policy
threshold is violated or any auth signal is missing or any content
risk flag is found. The verdict must include at least one blocker
reason and must signal `needs_human` escalation.
- `risk_level: block` and `preflight_clear: false` if any hard
violation (auth fails and bounce rate exceeds policy) is present.
7. Emit the `send_risk_verdict` described below.

## Output Contract

```yaml
send_risk_verdict:
risk_level: pass | hold | block
preflight_clear: bool
blockers:
- string
evidence_summary:
auth_signals_verified: object
list_hygiene_metrics: object
content_risk_flags: [string]
policy_thresholds_applied: object
decision_refusal:
reason: string | null
```

The verdict fails closed: when a signal or metric is missing, the verdict
sets `preflight_clear: false`, lists the missing input as a blocker, and
does not invent a missing value.

## Inputs

```yaml
campaign_draft:
from: string
subject: string
content_digest: string
list_metadata:
size: number
bounce_rate: number
complaint_rate: number
freshness_days: number
sender_auth_posture:
spf_pass: bool
dkim_pass: bool
dmarc_pass: bool
warm_up_days: number
```

## Outputs

```yaml
send_risk_verdict:
risk_level: pass | hold | block
preflight_clear: bool
blockers: [string]
evidence_summary:
auth_signals_verified:
spf_pass: bool
dkim_pass: bool
dmarc_pass: bool
warm_up_days: number
list_hygiene_metrics:
size: number
bounce_rate: number
complaint_rate: number
freshness_days: number
content_risk_flags: [string]
policy_thresholds_applied:
bounce_rate_max: 0.05
complaint_rate_max: 0.001
freshness_days_max: 30
decision_refusal:
reason: string | null
```

The verdict binds into `send-as` preflight checks and blockers, where a
non-clear verdict prevents `send-as` from satisfying its preflight and
forces human approval. The `public_send` Effect belongs to `send-as`,
never to this skill, which delivers no message and never executes the
send.

## Verification

Two harness cases pin the verdict:

1. `low-risk-verified-sender`: a verified sender with clean list signals
and full authentication yields
`risk_level: pass`, `preflight_clear: true`, `blockers: []`.
2. `high-risk-incomplete-auth-poor-list`: DKIM does not pass and bounce
rate exceeds policy, yielding
`risk_level: hold`, `preflight_clear: false`, `blockers: [...]` with
no preflight clearance and a `needs_human` escalation.

The hosted registry harness reads only these two cases.

## Public value

Operators use `spam-risk-reviewer` as the pre-send gate for any campaign
whose authority they care about. A real user can install the skill with
`runx add jdjioe5-cpu/spam-risk-reviewer@<version>`, run it on any
campaign draft, and read the verdict into their downstream `send-as`
preflight check. The judgment never replaces a human approval lane; it
fails closed and refuses to clear preflight for any input that does not
explicitly clear all three auth signals and all three hygiene metrics.
171 changes: 171 additions & 0 deletions skills/spam-risk-reviewer/X.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
skill: spam-risk-reviewer
version: "0.1.0"

catalog:
kind: graph
audience: public
visibility: public
role: context

harness:
cases:
- name: spam-risk-reviewer-low-risk-verified-sender
runner: spam_risk
inputs:
campaign_draft:
from: ops@newsletter.example.com
subject: Weekly product digest - week 27
content_digest: Welcome back. Here is the weekly digest of product changes. No offers or urgency language.
list_metadata:
size: 18420
bounce_rate: 0.014
complaint_rate: 0.0003
freshness_days: 4
sender_auth_posture:
spf_pass: true
dkim_pass: true
dmarc_pass: true
warm_up_days: 220
operator_context: Use this verdict to gate the send-as preflight. The skill only judges; it does not dispatch the send.
caller:
answers:
agent_task.spam-risk-reviewer.output:
send_risk_verdict:
risk_level: pass
preflight_clear: true
blockers: []
evidence_summary:
auth_signals_verified:
spf_pass: true
dkim_pass: true
dmarc_pass: true
warm_up_days: 220
list_hygiene_metrics:
size: 18420
bounce_rate: 0.014
complaint_rate: 0.0003
freshness_days: 4
content_risk_flags: []
policy_thresholds_applied:
bounce_rate_max: 0.05
complaint_rate_max: 0.001
freshness_days_max: 30
decision_refusal:
reason: null
expect:
status: sealed
receipt:
schema: runx.receipt.v1
state: sealed
- name: spam-risk-reviewer-high-risk-incomplete-auth-poor-list
runner: spam_risk
inputs:
campaign_draft:
from: blast@promo.example.com
subject: CLICK HERE NOW for a once-in-a-lifetime free money offer
content_digest: Free money, click here now, urgent offer for the next 24 hours only.
list_metadata:
size: 6200
bounce_rate: 0.092
complaint_rate: 0.0024
freshness_days: 140
sender_auth_posture:
spf_pass: true
dkim_pass: false
dmarc_pass: true
warm_up_days: 7
operator_context: Block the send; route to human approval.
caller:
answers:
agent_task.spam-risk-reviewer.output:
send_risk_verdict:
risk_level: hold
preflight_clear: false
blockers:
- dkim_pass is false so the SPF+DKIM+DMARC must-pass policy fails.
- bounce_rate 0.092 exceeds policy threshold of 0.05.
- complaint_rate 0.0024 exceeds policy threshold of 0.001.
- freshness_days 140 exceeds policy threshold of 30.
- warm_up_days 7 is below the recommended 30-day warm-up floor.
- content risk flag present for free money pattern, click here now pattern, and urgency window.
evidence_summary:
auth_signals_verified:
spf_pass: true
dkim_pass: false
dmarc_pass: true
warm_up_days: 7
list_hygiene_metrics:
size: 6200
bounce_rate: 0.092
complaint_rate: 0.0024
freshness_days: 140
content_risk_flags:
- free money
- click here now
- urgency-window
policy_thresholds_applied:
bounce_rate_max: 0.05
complaint_rate_max: 0.001
freshness_days_max: 30
decision_refusal:
reason: needs_human
expect:
status: sealed
receipt:
schema: runx.receipt.v1
state: sealed
disposition: needs_human
- name: spam-risk-reviewer-missing-sender-auth-fails-closed
runner: spam_risk
inputs:
campaign_draft:
from: ops@newsletter.example.com
subject: Weekly product digest - missing auth posture
content_digest: Welcome back. Here is a normal digest, but auth evidence is intentionally absent.
list_metadata:
size: 18420
bounce_rate: 0.014
complaint_rate: 0.0003
freshness_days: 4
operator_context: Missing sender_auth_posture must fail closed; do not invent SPF, DKIM, or DMARC evidence.
expect:
status: failure
receipt:
schema: runx.receipt.v1
state: sealed
disposition: failed
reason_code: process_failed

runners:
spam_risk:
default: true
type: cli-tool
command: /usr/bin/env
args:
- node
- run.mjs
inputs:
campaign_draft:
type: json
required: true
description: Draft sender, subject, and content digest.
list_metadata:
type: json
required: true
description: List size, bounce rate, complaint rate, and freshness.
sender_auth_posture:
type: json
required: true
description: SPF, DKIM, DMARC, and warm-up posture.
operator_context:
type: string
required: false
description: Operator constraints for the preflight decision.
outputs:
send_risk_verdict: object
artifacts:
wrap_as: send_risk_verdict
packet: runx.send_risk.verdict.v1
execution:
disposition: completed
outcome_state: complete
17 changes: 17 additions & 0 deletions skills/spam-risk-reviewer/fixtures/harness-result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"status": "passed",
"case_count": 3,
"assertion_error_count": 0,
"assertion_errors": [],
"case_names": [
"spam-risk-reviewer-low-risk-verified-sender",
"spam-risk-reviewer-high-risk-incomplete-auth-poor-list",
"spam-risk-reviewer-missing-sender-auth-fails-closed"
],
"receipt_ids": [
"sha256:e5674f9e700662be6487b394b16b93583b5c8a884b873f140a07ab2eb799abe7",
"sha256:f99ae28abcc1ce69c01b876fef6a2c55a2fbd16c50e9ea8a2e90edd6a631f1ac",
"sha256:42985c54816619fcce6c2e526e2ace6e3462bf1aeadcd3f78bc46a317ca63173"
],
"graph_case_count": 0
}
Loading