Spaces:
Paused
feat: seed-variant pool + chain gate enforcement
Browse files- tasks.py: add TASK_VARIANTS with 2 alternative scenarios per task for 14
tasks (MED-003..006, HARD-001..006, CRIT-001..004); each variant has a
different correct_action discoverable only via investigation reveals;
pre-investigation observation is genuinely ambiguous - closes the
correct_action memorisation exploit
- environment.py: replace fragile threshold-based variant system with clean
seed-deterministic variant pool selection;
(seed * 1009 + task_idx * 17) % (n+1) gives good distribution across seeds;
seed=0 always uses base variants (all existing tests remain stable)
- environment.py: add multi-step chain gate to step_async - CRIT tasks with
chain_total > 1 require (chain_total - 1) investigation steps before a
terminal decision is accepted; blocked attempts return chain_gate_blocked
event with chain_steps_needed indicator and -0.05 reward signal
- environment.py +46 -74
- tasks.py +403 -0
|
@@ -24,7 +24,7 @@ from typing import Deque, List, Optional
|
|
| 24 |
|
| 25 |
from payops_env.grader import INVESTIGATION_BONUS, grade
|
| 26 |
from payops_env.models import PayOpsAction, PayOpsObservation, PayOpsState
|
| 27 |
-
from payops_env.tasks import ACTION_COSTS, TASKS, PayOpsTask
|
| 28 |
|
| 29 |
|
| 30 |
TERMINAL_ACTIONS = {"approve", "reject", "flag", "escalate", "hold"}
|
|
@@ -84,79 +84,27 @@ class PayOpsEnvironment:
|
|
| 84 |
jt.velocity_24h = max(0, t.velocity_24h + rng.randint(-3, 3))
|
| 85 |
jittered.append(jt)
|
| 86 |
self._tasks = jittered
|
| 87 |
-
|
| 88 |
-
#
|
| 89 |
-
#
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
if jt.amount >= 500.0:
|
| 109 |
-
jt.correct_action = "hold"
|
| 110 |
-
jt.partial_credit_actions = {"flag": 0.5, "escalate": 0.3}
|
| 111 |
-
|
| 112 |
-
elif jt.task_id == "MED-004":
|
| 113 |
-
# Base risk_score=0.58 (jittered ±0.03).
|
| 114 |
-
# >= 0.62: risk crosses the senior-review threshold → escalate.
|
| 115 |
-
if jt.risk_score >= 0.62:
|
| 116 |
-
jt.correct_action = "escalate"
|
| 117 |
-
jt.partial_credit_actions = {"flag": 0.5, "hold": 0.3}
|
| 118 |
-
|
| 119 |
-
elif jt.task_id == "MED-006":
|
| 120 |
-
# Base risk_score=0.56, amount=120k real-estate advance.
|
| 121 |
-
# Low jitter: if risk drops below 0.50 and amount < 110k → hold not escalate.
|
| 122 |
-
if jt.risk_score < 0.50 and jt.amount < 110_000:
|
| 123 |
-
jt.correct_action = "hold"
|
| 124 |
-
jt.partial_credit_actions = {"flag": 0.4, "escalate": 0.5}
|
| 125 |
-
|
| 126 |
-
elif jt.task_id == "HARD-001":
|
| 127 |
-
# Base risk_score=0.18 (poisoned), counterparty=watchlist.
|
| 128 |
-
# If velocity_1h jitters up to >= 3: pattern is undeniable → reject not escalate.
|
| 129 |
-
if (jt.velocity_1h or 0) >= 3:
|
| 130 |
-
jt.correct_action = "reject"
|
| 131 |
-
jt.partial_credit_actions = {"escalate": 0.5, "flag": 0.4, "hold": 0.3}
|
| 132 |
-
|
| 133 |
-
elif jt.task_id == "HARD-006":
|
| 134 |
-
# Base amount=3200 EUR, risk=0.63 (ghost account / mule reactivation).
|
| 135 |
-
# High spike: if amount >= 3700 → escalate urgently, not just flag.
|
| 136 |
-
if jt.amount >= 3_700:
|
| 137 |
-
jt.correct_action = "escalate"
|
| 138 |
-
jt.partial_credit_actions = {"flag": 0.5, "hold": 0.4}
|
| 139 |
-
|
| 140 |
-
elif jt.task_id == "CRIT-001":
|
| 141 |
-
# Base risk_score=0.59 (PE wire, legit).
|
| 142 |
-
# If risk jitters above 0.80 → escalate for extra scrutiny before approve.
|
| 143 |
-
if jt.risk_score >= 0.80:
|
| 144 |
-
jt.correct_action = "escalate"
|
| 145 |
-
jt.partial_credit_actions = {"flag": 0.4, "hold": 0.35, "approve": 0.3}
|
| 146 |
-
|
| 147 |
-
elif jt.task_id == "CRIT-003":
|
| 148 |
-
# Base amount=680k trade-based ML (escalate).
|
| 149 |
-
# If amount jitters very high (>= 780k) → reject outright; over-invoicing extreme.
|
| 150 |
-
if jt.amount >= 780_000:
|
| 151 |
-
jt.correct_action = "reject"
|
| 152 |
-
jt.partial_credit_actions = {"escalate": 0.5, "flag": 0.3}
|
| 153 |
-
|
| 154 |
-
elif jt.task_id == "CRIT-004":
|
| 155 |
-
# Base risk=0.81, geo-impossible ATO (reject).
|
| 156 |
-
# If risk jitters below 0.75 → hold pending contact_sender confirmation.
|
| 157 |
-
if jt.risk_score < 0.75:
|
| 158 |
-
jt.correct_action = "hold"
|
| 159 |
-
jt.partial_credit_actions = {"escalate": 0.5, "reject": 0.4}
|
| 160 |
self._current_task = self._tasks[0]
|
| 161 |
self._used_inv = {}
|
| 162 |
self._sar_filed = set()
|
|
@@ -197,6 +145,30 @@ class PayOpsEnvironment:
|
|
| 197 |
task_id = task.task_id
|
| 198 |
cost = ACTION_COSTS.get(action_type, 0.0)
|
| 199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
# Deduct cost
|
| 201 |
self._state.budget_spent = round(self._state.budget_spent + cost, 4)
|
| 202 |
self._state.step_count += 1
|
|
|
|
| 24 |
|
| 25 |
from payops_env.grader import INVESTIGATION_BONUS, grade
|
| 26 |
from payops_env.models import PayOpsAction, PayOpsObservation, PayOpsState
|
| 27 |
+
from payops_env.tasks import ACTION_COSTS, TASK_VARIANTS, TASKS, PayOpsTask
|
| 28 |
|
| 29 |
|
| 30 |
TERMINAL_ACTIONS = {"approve", "reject", "flag", "escalate", "hold"}
|
|
|
|
| 84 |
jt.velocity_24h = max(0, t.velocity_24h + rng.randint(-3, 3))
|
| 85 |
jittered.append(jt)
|
| 86 |
self._tasks = jittered
|
| 87 |
+
|
| 88 |
+
# ── Variant pool selection ──────────────────────────────────────────────
|
| 89 |
+
# Each task in TASK_VARIANTS has 2 alternative scenarios (in addition to
|
| 90 |
+
# the base). The episode seed selects which scenario plays out, making
|
| 91 |
+
# the correct_action unknowable from the task_id alone — the agent MUST
|
| 92 |
+
# investigate to discover the decisive evidence.
|
| 93 |
+
#
|
| 94 |
+
# seed=0 is the canonical episode (all base variants); used by the test
|
| 95 |
+
# suite so that hardcoded expected rewards remain stable.
|
| 96 |
+
if episode_seed != 0:
|
| 97 |
+
for i, jt in enumerate(self._tasks):
|
| 98 |
+
variants = TASK_VARIANTS.get(jt.task_id, [])
|
| 99 |
+
if not variants:
|
| 100 |
+
continue
|
| 101 |
+
# variant_idx=0 → base (no changes); 1..N → variants[0..N-1]
|
| 102 |
+
variant_idx = (episode_seed * 1009 + i * 17) % (len(variants) + 1)
|
| 103 |
+
if variant_idx == 0:
|
| 104 |
+
continue
|
| 105 |
+
overrides = variants[variant_idx - 1]
|
| 106 |
+
for key, val in overrides.items():
|
| 107 |
+
setattr(jt, key, val)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
self._current_task = self._tasks[0]
|
| 109 |
self._used_inv = {}
|
| 110 |
self._sar_filed = set()
|
|
|
|
| 145 |
task_id = task.task_id
|
| 146 |
cost = ACTION_COSTS.get(action_type, 0.0)
|
| 147 |
|
| 148 |
+
# ── MULTI-STEP CHAIN GATE ─────────────────────────────────────────────
|
| 149 |
+
# Critical tasks with chain_total > 1 require (chain_total − 1)
|
| 150 |
+
# investigation sub-actions before a terminal decision is accepted.
|
| 151 |
+
# Blocked attempts return a helpful message without advancing the task.
|
| 152 |
+
if action_type in TERMINAL_ACTIONS:
|
| 153 |
+
chain_min = max(0, getattr(task, "chain_total", 1) - 1)
|
| 154 |
+
inv_done = len(self._used_inv.get(task_id, set()))
|
| 155 |
+
if chain_min > 0 and inv_done < chain_min:
|
| 156 |
+
needed = chain_min - inv_done
|
| 157 |
+
return self._make_observation(
|
| 158 |
+
reward=-0.05,
|
| 159 |
+
done=False,
|
| 160 |
+
info={
|
| 161 |
+
"event": "chain_gate_blocked",
|
| 162 |
+
"chain_status": "investigation_required",
|
| 163 |
+
"chain_steps_needed": needed,
|
| 164 |
+
"message": (
|
| 165 |
+
f"This {task.difficulty} transaction requires {needed} "
|
| 166 |
+
f"more investigation step(s) before a terminal decision. "
|
| 167 |
+
f"Please investigate first."
|
| 168 |
+
),
|
| 169 |
+
},
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
# Deduct cost
|
| 173 |
self._state.budget_spent = round(self._state.budget_spent + cost, 4)
|
| 174 |
self._state.step_count += 1
|
|
@@ -701,4 +701,407 @@ ACTION_COSTS: Dict[str, float] = {
|
|
| 701 |
"file_sar": 0.05, # intentionally cheap — incentivise regulatory compliance
|
| 702 |
}
|
| 703 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
|
|
|
|
| 701 |
"file_sar": 0.05, # intentionally cheap — incentivise regulatory compliance
|
| 702 |
}
|
| 703 |
|
| 704 |
+
# ---------------------------------------------------------------------------
|
| 705 |
+
# Task variant pools — anti-memorisation & investigation-gating
|
| 706 |
+
# ---------------------------------------------------------------------------
|
| 707 |
+
# Each entry is a list of override dicts for alternative scenarios of that task.
|
| 708 |
+
# Variant index 0 always means "use base task" (no overrides applied).
|
| 709 |
+
# Variants 1..N override the fields listed, so the correct_action is only
|
| 710 |
+
# determinable after performing the required investigation sub-actions.
|
| 711 |
+
# The variant is selected deterministically from the episode seed in environment.py.
|
| 712 |
+
#
|
| 713 |
+
# Key design constraint: the BASE observation (pre-investigation) must be
|
| 714 |
+
# genuinely ambiguous — investigation reveals ARE the decisive evidence.
|
| 715 |
+
# ---------------------------------------------------------------------------
|
| 716 |
+
TASK_VARIANTS: Dict[str, List[Dict]] = {
|
| 717 |
+
|
| 718 |
+
# ── MEDIUM ────────────────────────────────────────────────────────────
|
| 719 |
+
|
| 720 |
+
"MED-003": [
|
| 721 |
+
# v1: unauthorised recurring billing → hold to freeze
|
| 722 |
+
{
|
| 723 |
+
"correct_action": "hold",
|
| 724 |
+
"partial_credit_actions": {"flag": 0.5, "escalate": 0.3},
|
| 725 |
+
"inspect_reveal": (
|
| 726 |
+
"Merchant billing records show 3 prior declined charges this month "
|
| 727 |
+
"from different cards linked to the same subscriber IP. Pattern is "
|
| 728 |
+
"consistent with credential stuffing / unauthorised recurring charge. "
|
| 729 |
+
"Freeze payment and contact customer before releasing."
|
| 730 |
+
),
|
| 731 |
+
},
|
| 732 |
+
# v2: confirmed annual plan upgrade → approve
|
| 733 |
+
{
|
| 734 |
+
"correct_action": "approve",
|
| 735 |
+
"partial_credit_actions": {"flag": 0.3},
|
| 736 |
+
"inspect_reveal": (
|
| 737 |
+
"Customer service log: subscriber upgraded to annual plan via phone "
|
| 738 |
+
"this morning. $449.97 = 3 × monthly fee (annual discount applied). "
|
| 739 |
+
"Merchant has confirmed the charge. Safe to approve."
|
| 740 |
+
),
|
| 741 |
+
},
|
| 742 |
+
],
|
| 743 |
+
|
| 744 |
+
"MED-004": [
|
| 745 |
+
# v1: structuring pattern across exchanges → escalate
|
| 746 |
+
{
|
| 747 |
+
"correct_action": "escalate",
|
| 748 |
+
"partial_credit_actions": {"flag": 0.5, "hold": 0.3},
|
| 749 |
+
"inspect_reveal": (
|
| 750 |
+
"Sender has made 7 crypto exchange payments in 5 days totalling $28k "
|
| 751 |
+
"across 3 different platforms. Amounts stay just below exchange reporting "
|
| 752 |
+
"limits. Pattern matches structuring. Escalate for AML review."
|
| 753 |
+
),
|
| 754 |
+
},
|
| 755 |
+
# v2: clean recurring investment → approve
|
| 756 |
+
{
|
| 757 |
+
"correct_action": "approve",
|
| 758 |
+
"partial_credit_actions": {"flag": 0.3},
|
| 759 |
+
"inspect_reveal": (
|
| 760 |
+
"Sender's 3-year payment history shows consistent quarterly crypto "
|
| 761 |
+
"purchases of similar size. Exchange is fully regulated and KYC-verified. "
|
| 762 |
+
"Amount within personal investment limits. No structuring indicators. Approve."
|
| 763 |
+
),
|
| 764 |
+
},
|
| 765 |
+
],
|
| 766 |
+
|
| 767 |
+
"MED-005": [
|
| 768 |
+
# v1: KYC discrepancy found → escalate
|
| 769 |
+
{
|
| 770 |
+
"correct_action": "escalate",
|
| 771 |
+
"partial_credit_actions": {"flag": 0.5, "hold": 0.3},
|
| 772 |
+
"kyc_reveal": (
|
| 773 |
+
"KYC expired because the compliance team found discrepancies between "
|
| 774 |
+
"the business registration address and Companies House records. The "
|
| 775 |
+
"case has been flagged for investigation by the KYC team. Escalate — "
|
| 776 |
+
"do not approve or hold without senior sign-off."
|
| 777 |
+
),
|
| 778 |
+
},
|
| 779 |
+
# v2: KYC renewed, routine payroll → approve
|
| 780 |
+
{
|
| 781 |
+
"correct_action": "approve",
|
| 782 |
+
"partial_credit_actions": {"hold": 0.4},
|
| 783 |
+
"kyc_reveal": (
|
| 784 |
+
"KYC renewal was submitted 10 days ago and completed processing "
|
| 785 |
+
"yesterday. All checks passed. The lapse was an administrative "
|
| 786 |
+
"oversight only. Payroll has an 18-month history with no anomalies. "
|
| 787 |
+
"Approve and process."
|
| 788 |
+
),
|
| 789 |
+
},
|
| 790 |
+
],
|
| 791 |
+
|
| 792 |
+
"MED-006": [
|
| 793 |
+
# v1: forged purchase agreement → reject
|
| 794 |
+
{
|
| 795 |
+
"correct_action": "reject",
|
| 796 |
+
"partial_credit_actions": {"escalate": 0.4, "flag": 0.3},
|
| 797 |
+
"docs_reveal": (
|
| 798 |
+
"Purchase agreement is a forgery: the notary seal serial number "
|
| 799 |
+
"does not exist in the notary registry; the property title number "
|
| 800 |
+
"returns no match in Land Registry; the 'escrow agent' is not "
|
| 801 |
+
"registered with the Council for Licensed Conveyancers. "
|
| 802 |
+
"Classic conveyancing fraud — reject immediately."
|
| 803 |
+
),
|
| 804 |
+
},
|
| 805 |
+
# v2: title dispute — hold pending resolution
|
| 806 |
+
{
|
| 807 |
+
"correct_action": "hold",
|
| 808 |
+
"partial_credit_actions": {"escalate": 0.5, "flag": 0.3},
|
| 809 |
+
"docs_reveal": (
|
| 810 |
+
"SPA is authentic and escrow agent is licensed. However, a co-owner "
|
| 811 |
+
"has filed a title dispute on the property. The buyer's solicitor "
|
| 812 |
+
"advises funds should be held in escrow until the dispute is resolved "
|
| 813 |
+
"(expected 3–4 weeks). Hold pending legal clearance."
|
| 814 |
+
),
|
| 815 |
+
},
|
| 816 |
+
],
|
| 817 |
+
|
| 818 |
+
# ── HARD ──────────────────────────────────────────────────────────────
|
| 819 |
+
|
| 820 |
+
"HARD-001": [
|
| 821 |
+
# v1: deeper forensics confirm active fraud ring → reject
|
| 822 |
+
{
|
| 823 |
+
"correct_action": "reject",
|
| 824 |
+
"partial_credit_actions": {"escalate": 0.5, "flag": 0.3},
|
| 825 |
+
"inspect_reveal": (
|
| 826 |
+
"Cross-database query: same receiver account (payee@solicitor-uk.com) "
|
| 827 |
+
"appeared in 4 prior rejected fraud wires this quarter, total £190k. "
|
| 828 |
+
"Funds clear to offshore within 45 min each time. ML score was "
|
| 829 |
+
"deliberately poisoned via clean-account seeding. Reject immediately."
|
| 830 |
+
),
|
| 831 |
+
},
|
| 832 |
+
# v2: flags are false positives, ML score correct → approve
|
| 833 |
+
{
|
| 834 |
+
"correct_action": "approve",
|
| 835 |
+
"partial_credit_actions": {"escalate": 0.4, "flag": 0.3},
|
| 836 |
+
"inspect_reveal": (
|
| 837 |
+
"Manual flag investigation: 'solicitor_mule_pattern' rule mis-fired "
|
| 838 |
+
"on a legitimate SRA-registered firm (reg. SRA-443210) that rebranded "
|
| 839 |
+
"last month, causing a new account. 'New_account_7d' flag triggered "
|
| 840 |
+
"by the rebrand. ALL flags are false positives. ML risk score 0.18 is "
|
| 841 |
+
"accurate. Safe to approve."
|
| 842 |
+
),
|
| 843 |
+
},
|
| 844 |
+
],
|
| 845 |
+
|
| 846 |
+
"HARD-002": [
|
| 847 |
+
# v1: new legitimate supplier, flags were incorrect → approve
|
| 848 |
+
{
|
| 849 |
+
"correct_action": "approve",
|
| 850 |
+
"partial_credit_actions": {"flag": 0.4, "hold": 0.3},
|
| 851 |
+
"contact_reveal": (
|
| 852 |
+
"Sender confirmed: mule_relay_2287@fastpay.io is their new supplier "
|
| 853 |
+
"in Singapore. The VPN flagged by geo-risk monitoring is the company's "
|
| 854 |
+
"standard corporate security policy. Purchase order #PO-44821 is on "
|
| 855 |
+
"file matching the amount. Safe to approve."
|
| 856 |
+
),
|
| 857 |
+
},
|
| 858 |
+
# v2: inconsistent story, AML review needed → escalate
|
| 859 |
+
{
|
| 860 |
+
"correct_action": "escalate",
|
| 861 |
+
"partial_credit_actions": {"reject": 0.4, "flag": 0.3},
|
| 862 |
+
"contact_reveal": (
|
| 863 |
+
"Contacted sender: story is vague and inconsistent across two "
|
| 864 |
+
"follow-up calls. Cannot confirm supplier identity. No purchase order "
|
| 865 |
+
"provided. The receiver's business registration cannot be located. "
|
| 866 |
+
"Cannot approve or reject — escalate to AML for investigation."
|
| 867 |
+
),
|
| 868 |
+
},
|
| 869 |
+
],
|
| 870 |
+
|
| 871 |
+
"HARD-003": [
|
| 872 |
+
# v1: confirmed structuring but scale needs MLRO → escalate
|
| 873 |
+
{
|
| 874 |
+
"correct_action": "escalate",
|
| 875 |
+
"partial_credit_actions": {"reject": 0.5, "flag": 0.3},
|
| 876 |
+
"inspect_reveal": (
|
| 877 |
+
"Pattern strongly resembles CTR structuring. However, the total "
|
| 878 |
+
"aggregate ($27,750) and the involvement of 3 related entities "
|
| 879 |
+
"means this crosses the threshold for mandatory MLRO referral under "
|
| 880 |
+
"internal policy. Escalate rather than unilaterally reject."
|
| 881 |
+
),
|
| 882 |
+
},
|
| 883 |
+
# v2: same UBO transfers are authorised corporate restructuring → hold
|
| 884 |
+
{
|
| 885 |
+
"correct_action": "hold",
|
| 886 |
+
"partial_credit_actions": {"reject": 0.4, "escalate": 0.5},
|
| 887 |
+
"inspect_reveal": (
|
| 888 |
+
"Same-UBO transfers are documented in a board resolution dated last "
|
| 889 |
+
"week authorising an internal corporate restructuring. KYC failure "
|
| 890 |
+
"was due to an ID document resubmission still in queue. Structuring "
|
| 891 |
+
"indicators are coincidental. Hold pending KYC re-verification."
|
| 892 |
+
),
|
| 893 |
+
},
|
| 894 |
+
],
|
| 895 |
+
|
| 896 |
+
"HARD-004": [
|
| 897 |
+
# v1: unrecognised SWIFT BIC, potential impersonation → reject
|
| 898 |
+
{
|
| 899 |
+
"correct_action": "reject",
|
| 900 |
+
"partial_credit_actions": {"escalate": 0.5, "flag": 0.3},
|
| 901 |
+
"inspect_reveal": (
|
| 902 |
+
"SWIFT BIC vostro@bank-b-swift.com resolved to a bank that does NOT "
|
| 903 |
+
"appear in the correspondent banking agreement registry. The BIC is "
|
| 904 |
+
"visually similar to a legitimate bank (one character substituted). "
|
| 905 |
+
"Likely impersonation of a correspondent partner — reject and alert."
|
| 906 |
+
),
|
| 907 |
+
},
|
| 908 |
+
# v2: ghost employees detected → flag for payroll audit
|
| 909 |
+
{
|
| 910 |
+
"correct_action": "flag",
|
| 911 |
+
"partial_credit_actions": {"approve": 0.5, "hold": 0.4},
|
| 912 |
+
"inspect_reveal": (
|
| 913 |
+
"FX settlement accounts are SWIFT-verified and legitimate. However, "
|
| 914 |
+
"the initiating staff workflow shows this $4.2M wire was split from "
|
| 915 |
+
"a larger batch that included 2 non-existent internal accounts. "
|
| 916 |
+
"Flag for payroll/treasury audit before releasing."
|
| 917 |
+
),
|
| 918 |
+
},
|
| 919 |
+
],
|
| 920 |
+
|
| 921 |
+
"HARD-005": [
|
| 922 |
+
# v1: confirmed insider fraud, no response from staff → reject
|
| 923 |
+
{
|
| 924 |
+
"correct_action": "reject",
|
| 925 |
+
"partial_credit_actions": {"escalate": 0.5, "flag": 0.3},
|
| 926 |
+
"contact_reveal": (
|
| 927 |
+
"Staff member's phone is disconnected; email bounced. HR confirms "
|
| 928 |
+
"the employee was placed on garden leave 2 days ago pending "
|
| 929 |
+
"investigation. Receiver account drained offshore within hours of "
|
| 930 |
+
"prior similar payments. Insider fraud confirmed — reject and freeze."
|
| 931 |
+
),
|
| 932 |
+
},
|
| 933 |
+
# v2: authorized vendor payment, staff working late → approve
|
| 934 |
+
{
|
| 935 |
+
"correct_action": "approve",
|
| 936 |
+
"partial_credit_actions": {"escalate": 0.4, "flag": 0.3},
|
| 937 |
+
"contact_reveal": (
|
| 938 |
+
"Staff member confirmed: personal Gmail was added as a forwarding "
|
| 939 |
+
"alias last month when the vendor switched to a personal invoicing "
|
| 940 |
+
"service. Contract reference VC-2024-189 verified by procurement. "
|
| 941 |
+
"After-hours initiation matches remote work schedule. Approve."
|
| 942 |
+
),
|
| 943 |
+
},
|
| 944 |
+
],
|
| 945 |
+
|
| 946 |
+
"HARD-006": [
|
| 947 |
+
# v1: confirmed active mule network → reject
|
| 948 |
+
{
|
| 949 |
+
"correct_action": "reject",
|
| 950 |
+
"partial_credit_actions": {"escalate": 0.5, "flag": 0.4},
|
| 951 |
+
"inspect_reveal": (
|
| 952 |
+
"Receiver account confirmed to be receiving from 12 other dormant "
|
| 953 |
+
"accounts this week (total €78k). All funds exit to the same offshore "
|
| 954 |
+
"cluster within 2 hours. Active money-mule aggregation network. "
|
| 955 |
+
"Reject and file SAR immediately."
|
| 956 |
+
),
|
| 957 |
+
},
|
| 958 |
+
# v2: estate probate settlement, legitimate size → escalate
|
| 959 |
+
{
|
| 960 |
+
"correct_action": "escalate",
|
| 961 |
+
"partial_credit_actions": {"flag": 0.6, "hold": 0.4},
|
| 962 |
+
"inspect_reveal": (
|
| 963 |
+
"Account was frozen under a probate order that was lifted 3 days ago. "
|
| 964 |
+
"Estate settlement documentation is on file. Amount (€3,200) is "
|
| 965 |
+
"consistent with a partial distribution from the estate. Legitimate "
|
| 966 |
+
"reactivation — escalate for senior approval given the reactivation flag."
|
| 967 |
+
),
|
| 968 |
+
},
|
| 969 |
+
],
|
| 970 |
+
|
| 971 |
+
# ── CRITICAL ──────────────────────────────────────────────────────────
|
| 972 |
+
|
| 973 |
+
"CRIT-001": [
|
| 974 |
+
# v1: deepfake deal, BEC fraud → reject
|
| 975 |
+
{
|
| 976 |
+
"correct_action": "reject",
|
| 977 |
+
"partial_credit_actions": {"escalate": 0.4, "hold": 0.35, "flag": 0.3},
|
| 978 |
+
"inspect_reveal": (
|
| 979 |
+
"PE firm name verified but domain is a typo-squatting lookalike "
|
| 980 |
+
"(pe-flrm.com vs pe-firm.com). No SEC filing exists for this deal. "
|
| 981 |
+
"The 'press announcement' URL leads to a page created 6 days ago. "
|
| 982 |
+
"Classic BEC / CEO impersonation fraud."
|
| 983 |
+
),
|
| 984 |
+
"docs_reveal": (
|
| 985 |
+
"'Signed SPA' is a doctored template — notarization certificate "
|
| 986 |
+
"serial number LN-2024-00512 does not exist in the notary registry. "
|
| 987 |
+
"Acquisition is fabricated. Reject and alert security team."
|
| 988 |
+
),
|
| 989 |
+
},
|
| 990 |
+
# v2: legitimate deal, wrong destination account → hold
|
| 991 |
+
{
|
| 992 |
+
"correct_action": "hold",
|
| 993 |
+
"partial_credit_actions": {"approve": 0.4, "escalate": 0.5},
|
| 994 |
+
"inspect_reveal": (
|
| 995 |
+
"PE firm and deal confirmed as genuine (SEC filing #0001234567). "
|
| 996 |
+
"Press announcement verified with three external sources. Acquisition "
|
| 997 |
+
"is legitimate. However, the destination account does not match the "
|
| 998 |
+
"independent escrow account specified in clause 7.3 of the SPA."
|
| 999 |
+
),
|
| 1000 |
+
"docs_reveal": (
|
| 1001 |
+
"SPA authenticated by legal team. Per clause 7.3, all milestone "
|
| 1002 |
+
"payments must route to independent escrow account "
|
| 1003 |
+
"escrow@trustco-escrow.com, not directly to the acquisition target. "
|
| 1004 |
+
"Hold and redirect to the correct escrow account."
|
| 1005 |
+
),
|
| 1006 |
+
},
|
| 1007 |
+
],
|
| 1008 |
+
|
| 1009 |
+
"CRIT-002": [
|
| 1010 |
+
# v1: same SME, multiple business accounts — coincidental threshold → hold
|
| 1011 |
+
{
|
| 1012 |
+
"correct_action": "hold",
|
| 1013 |
+
"partial_credit_actions": {"reject": 0.5, "escalate": 0.4},
|
| 1014 |
+
"inspect_reveal": (
|
| 1015 |
+
"The 3 flagged accounts all belong to the same registered SME "
|
| 1016 |
+
"(Companies House reg. 09876543) using separate business accounts "
|
| 1017 |
+
"for different cost centres. The $4,900 pattern matches their "
|
| 1018 |
+
"internal expense approval limit — not structuring intent. "
|
| 1019 |
+
"KYC re-verification recommended before releasing. Hold."
|
| 1020 |
+
),
|
| 1021 |
+
},
|
| 1022 |
+
# v2: ring is larger (9 accounts), MLRO escalation needed → escalate
|
| 1023 |
+
{
|
| 1024 |
+
"correct_action": "escalate",
|
| 1025 |
+
"partial_credit_actions": {"reject": 0.5, "flag": 0.3},
|
| 1026 |
+
"inspect_reveal": (
|
| 1027 |
+
"Network analysis reveals the ring involves 9 accounts total (not 3), "
|
| 1028 |
+
"all created on the same day, all funnelling to the same offshore "
|
| 1029 |
+
"collector. Aggregate total is $42k. Volume and complexity require "
|
| 1030 |
+
"MLRO involvement and a formal SAR — escalate rather than unilaterally reject."
|
| 1031 |
+
),
|
| 1032 |
+
},
|
| 1033 |
+
],
|
| 1034 |
+
|
| 1035 |
+
"CRIT-003": [
|
| 1036 |
+
# v1: legitimate specialized goods, market price correct → approve
|
| 1037 |
+
{
|
| 1038 |
+
"correct_action": "approve",
|
| 1039 |
+
"partial_credit_actions": {"escalate": 0.5, "hold": 0.3},
|
| 1040 |
+
"docs_reveal": (
|
| 1041 |
+
"Independent commodity valuation confirms: these are custom precision "
|
| 1042 |
+
"industrial components for semiconductor manufacturing. Verified market "
|
| 1043 |
+
"price: $1,310–1,420 per unit. Invoice price of $1,360 is within "
|
| 1044 |
+
"normal range. No mismatch. Documentation is clean."
|
| 1045 |
+
),
|
| 1046 |
+
"inspect_reveal": (
|
| 1047 |
+
"Full shipping manifest verified: 500 units dispatched per bill of "
|
| 1048 |
+
"lading BoL-HK-2024-8821. Customs clearance certified in both "
|
| 1049 |
+
"Hong Kong and the destination country. No discrepancies."
|
| 1050 |
+
),
|
| 1051 |
+
},
|
| 1052 |
+
# v2: criminal-scale TBML, reject immediately
|
| 1053 |
+
{
|
| 1054 |
+
"correct_action": "reject",
|
| 1055 |
+
"partial_credit_actions": {"escalate": 0.5, "flag": 0.3},
|
| 1056 |
+
"docs_reveal": (
|
| 1057 |
+
"Invoice declares 500 units @ $1,360 = $680k. Independent valuation: "
|
| 1058 |
+
"market price is $250/unit. Over-invoiced by 5.4×. Matches a "
|
| 1059 |
+
"trade-based money laundering typology filed with FATF last year. "
|
| 1060 |
+
"Criminal-level TBML — reject and file SAR immediately."
|
| 1061 |
+
),
|
| 1062 |
+
"inspect_reveal": (
|
| 1063 |
+
"Shipping records show only 100 units dispatched vs 500 invoiced. "
|
| 1064 |
+
"Invoice-to-goods ratio 5.4× above market. Direct evidence of false "
|
| 1065 |
+
"customs declarations. Reject immediately."
|
| 1066 |
+
),
|
| 1067 |
+
},
|
| 1068 |
+
],
|
| 1069 |
+
|
| 1070 |
+
"CRIT-004": [
|
| 1071 |
+
# v1: CEO delegated to CFO who was in Lagos — hold for confirmation
|
| 1072 |
+
{
|
| 1073 |
+
"correct_action": "hold",
|
| 1074 |
+
"partial_credit_actions": {"escalate": 0.5, "reject": 0.4},
|
| 1075 |
+
"inspect_reveal": (
|
| 1076 |
+
"Geo-impossible login flagged. However, a delegation email from the "
|
| 1077 |
+
"CEO was sent 2 hours ago via their verified corporate account granting "
|
| 1078 |
+
"the CFO emergency signing authority for this exact transfer. "
|
| 1079 |
+
"CFO was in Lagos for a board meeting (flight records available). "
|
| 1080 |
+
),
|
| 1081 |
+
"contact_reveal": (
|
| 1082 |
+
"CEO confirmed via secure callback: CFO had full authority to initiate "
|
| 1083 |
+
"this transfer. Delegation is documented in the board minutes. "
|
| 1084 |
+
"Hold briefly to verify the papertrail, then release."
|
| 1085 |
+
),
|
| 1086 |
+
},
|
| 1087 |
+
# v2: CEO unreachable, dual-approval controls met, escalate
|
| 1088 |
+
{
|
| 1089 |
+
"correct_action": "escalate",
|
| 1090 |
+
"partial_credit_actions": {"reject": 0.5, "hold": 0.4},
|
| 1091 |
+
"inspect_reveal": (
|
| 1092 |
+
"Account takeover strongly suspected. CEO's registered number is "
|
| 1093 |
+
"unreachable (OOO message). However, the transfer went through the "
|
| 1094 |
+
"standard dual-approval workflow with two internal approvers (both "
|
| 1095 |
+
"records look genuine). Outcome ambiguous — escalate to Fraud "
|
| 1096 |
+
"Operations for emergency investigation."
|
| 1097 |
+
),
|
| 1098 |
+
"contact_reveal": (
|
| 1099 |
+
"Cannot reach CEO. Deputy CFO states they approved the transfer but "
|
| 1100 |
+
"cannot confirm whether the CEO or an impersonator initiated it. "
|
| 1101 |
+
"Escalate immediately to Fraud Ops and place a temporary hold."
|
| 1102 |
+
),
|
| 1103 |
+
},
|
| 1104 |
+
],
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
|