padmapriyagosakan commited on
Commit
436d56f
·
1 Parent(s): 3f78483

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

Files changed (2) hide show
  1. environment.py +46 -74
  2. tasks.py +403 -0
environment.py CHANGED
@@ -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
- # Apply jitter variants: for borderline tasks the correct_action can change
88
- # based on jittered values, preventing memorisation of fixed correct answers.
89
- # Variants cover 10 of 20 tasks for broad anti-memorisation coverage.
90
- for jt in self._tasks:
91
- if jt.task_id == "EASY-004":
92
- # Base velocity_1h=15 (jittered 12–18).
93
- # >= 17: card-clone severity warrants immediate escalation.
94
- if (jt.velocity_1h or 0) >= 17:
95
- jt.correct_action = "escalate"
96
- jt.partial_credit_actions = {"flag": 0.5, "hold": 0.4}
97
-
98
- elif jt.task_id == "MED-001":
99
- # Base risk_score=0.52, amount=85k (cross-border B2B).
100
- # Low jitter: if risk drops below 0.48 AND amount stays modest → approve.
101
- if jt.risk_score < 0.48 and jt.amount < 90_000:
102
- jt.correct_action = "approve"
103
- jt.partial_credit_actions = {"escalate": 0.5, "flag": 0.3}
104
-
105
- elif jt.task_id == "MED-003":
106
- # Base amount≈450 (jittered 382–540).
107
- # >= 500: spike is large enough to hold rather than flag.
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
tasks.py CHANGED
@@ -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