@o-lang/olang 1.2.38 → 1.3.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,24 @@ const path = require('path');
3
3
  const crypto = require('crypto'); // ✅ CRYPTOGRAPHIC AUDIT LOGS
4
4
 
5
5
  // ✅ O-Lang Kernel Version (Safety Logic & Governance Rules)
6
- const KERNEL_VERSION = '1.2.30-alpha'; // 🔁 Update when safety rules change
6
+ const KERNEL_VERSION = '1.3.0-alpha'; // 🔁 Bumped: PII redaction engine added
7
+
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ // ✅ NEW v1.3.0 — SEPARATED PATTERN SETS
10
+ //
11
+ // WHY: Previously _validateInputs and _validateLLMOutput both duplicated one
12
+ // giant flat list. PII redaction must ONLY replace PII tokens (phone numbers,
13
+ // BVNs, account numbers) — it must NOT replace financial intent phrases like
14
+ // "fi owo ranṣẹ" with "[TRANSFER_REDACTED]" in legitimate LLM prompts.
15
+ //
16
+ // Backward compatibility: all old patterns are preserved exactly. They are now
17
+ // organised into two methods:
18
+ // _getPIIPatterns() → used by new _redactPII()
19
+ // _getFinancialIntentPatterns() → used by _validateInputs / _validateLLMOutput
20
+ //
21
+ // BACKWARD COMPAT: _validateInputs still throws by default (MODE = 'block').
22
+ // Set OLANG_PII_MODE=redact in env to switch to non-throwing redaction mode.
23
+ // ─────────────────────────────────────────────────────────────────────────────
7
24
 
8
25
  class RuntimeAPI {
9
26
  constructor({ verbose = false } = {}) {
@@ -20,24 +37,362 @@ class RuntimeAPI {
20
37
  if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
21
38
  this.disallowedLogFile = path.join(logsDir, 'disallowed_resolvers.json');
22
39
  this.disallowedAttempts = [];
23
-
40
+
24
41
  // ✅ NEW: Database client setup
25
42
  this.dbClient = null;
26
43
  this._initDbClient();
27
-
44
+
28
45
  // ✅ NEW: Cryptographically verifiable audit logs
29
46
  this.auditLog = [];
30
47
  this.previousHash = 'GENESIS';
31
48
  this.auditLogPrivateKey = process.env.OLANG_AUDIT_PRIVATE_KEY;
32
49
  this.auditLogFile = path.join(logsDir, 'audit_log.json');
33
50
  this.enableAuditLog = process.env.OLANG_AUDIT_LOG === 'true';
34
-
51
+
52
+ // ✅ NEW v1.3.0 — PII operating mode
53
+ // 'block' → original behaviour: throw on PII (default, backward compat)
54
+ // 'redact' → new behaviour: replace PII tokens, continue execution
55
+ // 'redact-and-log' → redact + emit audit entry per redaction event
56
+ this.piiMode = process.env.OLANG_PII_MODE || 'block';
57
+
35
58
  if (this.enableAuditLog && this.verbose) {
36
59
  console.log('🔐 Cryptographically verifiable audit logging enabled');
37
60
  }
61
+
62
+ if (this.verbose && this.piiMode !== 'block') {
63
+ console.log(`🛡️ PII mode: ${this.piiMode}`);
64
+ }
65
+ }
66
+
67
+ // ================================
68
+ // ✅ NEW v1.3.0 — PII-ONLY PATTERN SET
69
+ //
70
+ // These patterns match concrete identifiers that can be replaced with a
71
+ // [TYPE_REDACTED] token without destroying the semantic meaning of a sentence.
72
+ // They cover every language listed on the O-Lang site.
73
+ // ================================
74
+
75
+ _getPIIPatterns() {
76
+ return [
77
+
78
+ // ── Phone Numbers ──────────────────────────────────────────────────────
79
+
80
+ // Nigeria (MTN, Airtel, Glo, 9mobile)
81
+ {
82
+ pattern: /\b(?:\+?234\s*[-.]?|0)(?:70|80|81|90|91)\d{8}\b/g,
83
+ capability: 'pii_phone',
84
+ lang: 'ng',
85
+ label: 'NG_PHONE'
86
+ },
87
+ // Kenya (+254 / 07xx / 01xx)
88
+ {
89
+ pattern: /\b(?:\+?254\s*[-.]?|0)(?:7[0-9]|1[01])\d{7}\b/g,
90
+ capability: 'pii_phone',
91
+ lang: 'ke',
92
+ label: 'KE_PHONE'
93
+ },
94
+ // South Africa (+27 / 0xx)
95
+ {
96
+ pattern: /\b(?:\+?27\s*[-.]?|0)[6-8]\d{8}\b/g,
97
+ capability: 'pii_phone',
98
+ lang: 'za',
99
+ label: 'ZA_PHONE'
100
+ },
101
+ // Ethiopia (+251)
102
+ {
103
+ pattern: /\b(?:\+?251\s*[-.]?|0)[79]\d{8}\b/g,
104
+ capability: 'pii_phone',
105
+ lang: 'et',
106
+ label: 'ET_PHONE'
107
+ },
108
+ // 🇬🇭 Ghana (+233) - MTN (24/54), Telecel (20/50), AT (26/56), Glo (23)
109
+ {
110
+ pattern: /\b(?:\+?233\s*[-.]?|0)(?:2[0346]|5[046])\d{7}\b/g,
111
+ capability: 'pii_phone',
112
+ lang: 'gh',
113
+ label: 'GH_PHONE'
114
+ },
115
+ // Generic international E.164
116
+ {
117
+ pattern: /\+(?!234|254|27\b|251|233)[1-9]\d{1,2}[-.\s]?\d{3,5}[-.\s]?\d{4,9}\b/g,
118
+ capability: 'pii_phone',
119
+ lang: 'intl',
120
+ label: 'INTL_PHONE'
121
+ },
122
+
123
+ // ── National Identity Numbers ──────────────────────────────────────────
124
+
125
+ // Nigeria BVN (11 digits)
126
+ {
127
+ pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/ig,
128
+ capability: 'pii_national_id',
129
+ lang: 'ng',
130
+ label: 'NG_BVN'
131
+ },
132
+ // Nigeria NIN (11 digits)
133
+ {
134
+ pattern: /\b(?:nin|national\s+identification\s+number)\b.{0,20}\d{11}/ig,
135
+ capability: 'pii_national_id',
136
+ lang: 'ng',
137
+ label: 'NG_NIN'
138
+ },
139
+ // 🇬🇭 Ghana Card (National ID) - Format: GHA-XXXXXXXXX-X
140
+ {
141
+ pattern: /\bGHA-\d{9}-\d\b/ig,
142
+ capability: 'pii_national_id',
143
+ lang: 'gh',
144
+ label: 'GH_CARD'
145
+ },
146
+ // South Africa ID (13 digits YYMMDD + gender + race + check)
147
+ {
148
+ pattern: /\b[0-9]{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01])[0-9]{4}[01][0-9]{2}\b/g,
149
+ capability: 'pii_national_id',
150
+ lang: 'za',
151
+ label: 'ZA_ID'
152
+ },
153
+ // Kenya Huduma / National ID (7-8 digits)
154
+ {
155
+ pattern: /\b(?:national\s+id|id\s+number|huduma\s+namba)\b.{0,10}\d{7,8}\b/ig,
156
+ capability: 'pii_national_id',
157
+ lang: 'ke',
158
+ label: 'KE_ID'
159
+ },
160
+
161
+ // ── Bank Account Numbers ───────────────────────────────────────────────
162
+
163
+ // Generic account reference (works across all listed languages)
164
+ // Added Twi/Akan terms: 'sika' (money), 'konte' (account)
165
+ {
166
+ pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#|compte|cuenta|konto|konte|sika\s+number|حساب|حساب\s+رقم|حسابي|akaunti\s+ya|nambari\s+ya\s+akaunti)\s*[:\-—–]?\s*(\d{6,18})\b/ig,
167
+ capability: 'pii_account',
168
+ lang: 'multi',
169
+ label: 'ACCOUNT_NUMBER'
170
+ },
171
+ // IBAN (EU + Africa SWIFT members)
172
+ {
173
+ pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g,
174
+ capability: 'pii_account',
175
+ lang: 'intl',
176
+ label: 'IBAN'
177
+ },
178
+
179
+ // ── Email Addresses ────────────────────────────────────────────────────
180
+ {
181
+ pattern: /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/g,
182
+ capability: 'pii_email',
183
+ lang: 'multi',
184
+ label: 'EMAIL'
185
+ },
186
+
187
+ // ── Deceptive completion claims (cross-language) ───────────────────────
188
+ // These belong in PII set because they mask fraudulent state
189
+ {
190
+ pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|verified|imethibitishwa|imefanikiwa|amthibitishwa|ti\s+da|ti\s+ṣe|gụnyere|kimefanyika|yamekamilika)\b/ig,
191
+ capability: 'pii_deceptive_claim',
192
+ lang: 'multi',
193
+ label: 'DECEPTIVE_CLAIM'
194
+ }
195
+ ];
38
196
  }
197
+ // ================================
198
+ // ✅ NEW v1.3.0 — FINANCIAL INTENT PATTERN SET (unchanged from v1.2.x)
199
+ //
200
+ // These patterns detect *intent* to perform a financial action. They are
201
+ // NOT suitable for token-level redaction because replacing "fi owo ranṣẹ"
202
+ // with "[TRANSFER_REDACTED]" would destroy the sentence. They are used by
203
+ // _validateInputs (block mode) and _validateLLMOutput (always blocks).
204
+ // ================================
205
+
206
+ _getFinancialIntentPatterns() {
207
+ return [
208
+
209
+ // ────────────────────────────────────────────────
210
+ // 🇳🇬 NIGERIAN LANGUAGES (Fixed Unicode Boundaries)
211
+ // ────────────────────────────────────────────────
212
+
213
+ // YORUBA: Removed trailing \b after 'ṣẹ' to fix Unicode matching
214
+ { pattern: /fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ)/i, capability: 'transfer', lang: 'yo' },
215
+ { pattern: /ranṣẹ\s+(?:owo|pesa|kuɗi|ego)/i, capability: 'transfer', lang: 'yo' },
216
+ { pattern: /fi\s+\w+\s+\w+\s+ranṣẹ/i, capability: 'transfer', lang: 'yo' }, // Catches "Fi 5000 naira ranṣẹ"
217
+ { pattern: /san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo)/i, capability: 'payment', lang: 'yo' },
218
+ { pattern: /gba\s+owo/i, capability: 'withdrawal', lang: 'yo' },
219
+ { pattern: /\bti\s+(?:fi|san|gba|da|lo)/i, capability: 'unauthorized_action', lang: 'yo' },
220
+ { pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)/i, capability: 'unauthorized_action', lang: 'yo' },
221
+ { pattern: /\b(mo\s+ti\s+(?:fi|san|gba))/i, capability: 'unauthorized_action', lang: 'yo' },
222
+
223
+ // HAUSA: ✅ FIXED - Aggressive Substring Match (No Boundaries)
224
+ { pattern: /aika.{0,30}ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
225
+ { pattern: /ciyar\s*(?:da)?/i, capability: 'transfer', lang: 'ha' },
226
+ { pattern: /shiga\s+ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
227
+ { pattern: /turo\s+.*\s+aika/i, capability: 'transfer', lang: 'ha' },
228
+ { pattern: /biya\s*(?:da)?/i, capability: 'payment', lang: 'ha' },
229
+ { pattern: /sahaw[ae]\s+ku(?:ɗ|d)i/iu, capability: 'withdrawal', lang: 'ha' },
230
+ { pattern: /(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)/i, capability: 'unauthorized_action', lang: 'ha' },
231
+ { pattern: /(?:za\s+a|za\s+ta)\s+(?:ciyar|biya)/i, capability: 'unauthorized_action', lang: 'ha' },
232
+ { pattern: /ina\s+(?:ciyar|biya|sahawa)/i, capability: 'unauthorized_action', lang: 'ha' },
233
+
234
+ // IGBO: Removed trailing \b after 'igo'
235
+ { pattern: /zipu\s+(?:ego|moni|isi|na)/i, capability: 'transfer', lang: 'ig' },
236
+ { pattern: /buru\s+(?:ego|moni|isi)/i, capability: 'transfer', lang: 'ig' },
237
+ { pattern: /zi\s+.*\s+zipu/i, capability: 'transfer', lang: 'ig' },
238
+ { pattern: /tinye\s+(?:ego|moni|isi)/i, capability: 'deposit', lang: 'ig' },
239
+ { pattern: /(?:ziri|bururu|tinyere|gbara)/i, capability: 'unauthorized_action', lang: 'ig' },
240
+ { pattern: /m\s+(?:ziri|buru|zipuru|tinyere)/i, capability: 'unauthorized_action', lang: 'ig' },
241
+
242
+ // SWAHILI: ✅ FIXED - Catch Conjugated Forms (ni-li-pe, a-li-pe)
243
+ { pattern: /tuma\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
244
+ { pattern: /pelek[ae]?\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
245
+ { pattern: /wasilisha/i, capability: 'transfer', lang: 'sw' },
246
+ { pattern: /\b\w*lip[ae]\w*/i, capability: 'payment', lang: 'sw' },
247
+ { pattern: /maliza\s+malipo/i, capability: 'payment', lang: 'sw' },
248
+ { pattern: /ongez[ae]?\s*(?:kiasi|pesa|fedha)/i, capability: 'deposit', lang: 'sw' },
249
+ { pattern: /wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa)/i, capability: 'deposit', lang: 'sw' },
250
+ { pattern: /nime(?:tuma|lipa|ongeza|weka|peleka)/i, capability: 'unauthorized_action', lang: 'sw' },
251
+ { pattern: /(?:ni|u|a|tu|m|wa|ki|vi|zi|i)\s*me\s*(?:ongeza|weka|tuma|peleka|lipa|wasilisha)/i, capability: 'unauthorized_action', lang: 'sw' },
252
+
253
+ // OTHER AFRICAN:
254
+ // Amharic: Match roots anywhere
255
+ { pattern: /\u120b\u12ad/u, capability: 'transfer', lang: 'am' },
256
+ { pattern: /\u1308\u1263/u, capability: 'deposit', lang: 'am' },
257
+ { pattern: /\u12ad\u134c\u120d/u, capability: 'payment', lang: 'am' },
258
+ { pattern: /[\u1200-\u137F]{0,4}(?:\u1270\u120b\u120b\u1348|\u120b\u12ad|\u12ad\u134c\u120d|\u1338\u121d\u122d|\u12c8\u1323|\u1308\u1263)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
39
259
 
260
+ // Somali
261
+ { pattern: /dir\s+(?:lacag|maal|qarsoon)/i, capability: 'transfer', lang: 'so' },
262
+ { pattern: /bixi|bixis\s*o/i, capability: 'payment', lang: 'so' },
263
+
264
+ // Zulu: ✅ FIXED - Handle Subject Concords (u-thumela, ngi-hlawule)
265
+ { pattern: /thumel/i, capability: 'transfer', lang: 'zu' }, // Matches root inside uthumela, ngithumela
266
+ { pattern: /thumel.*imali/i, capability: 'transfer', lang: 'zu' },
267
+ { pattern: /hlawul/i, capability: 'payment', lang: 'zu' }, // Matches root inside hlawula, ngihlawule
268
+ { pattern: /hlawul.*imali/i, capability: 'payment', lang: 'zu' },
269
+
270
+ // ────────────────────────────────────────────────
271
+ // 🇿🇦 XHOSA ✅ NEW v1.3.0
272
+ //
273
+ // Xhosa shares Nguni root structure with Zulu. Subject concords differ
274
+ // (ndi- / u- / ba-) but financial roots are near-identical.
275
+ // ────────────────────────────────────────────────
276
+ { pattern: /thumela?\b/i, capability: 'transfer', lang: 'xh' }, // thumela (send)
277
+ { pattern: /thumela?\s+imali/i, capability: 'transfer', lang: 'xh' }, // send money
278
+ { pattern: /hlawul/i, capability: 'payment', lang: 'xh' }, // pay (shared Nguni root)
279
+ { pattern: /beka\s+(?:imali|ingeniso)/i, capability: 'deposit', lang: 'xh' }, // deposit
280
+ { pattern: /rhola\s+imali/i, capability: 'withdrawal', lang: 'xh' }, // withdraw money
281
+ { pattern: /ndi(?:thumele|hlawule|beke|rhola)/i, capability: 'unauthorized_action', lang: 'xh' }, // 1st person perfect
282
+ { pattern: /u(?:thumele|hlawule|beke|rhola)/i, capability: 'unauthorized_action', lang: 'xh' }, // 3rd person perfect
283
+
284
+ // ────────────────────────────────────────────────
285
+ // 🇬🇭 GHANA: TWI (AKAN) ✅ NEW v1.3.0-alpha
286
+ //
287
+ // Twi is the most widely spoken language in Ghana.
288
+ // Financial roots: 'soma' (send), 'tua' (pay), 'fa' (take/use), 'kɔ' (go)
289
+ // ────────────────────────────────────────────────
290
+
291
+ // Transfer/Send Money
292
+ { pattern: /soma\s+(?:sika|money)/i, capability: 'transfer', lang: 'tw' }, // Send money
293
+ { pattern: /de\s+sika\s+ma/i, capability: 'transfer', lang: 'tw' }, // Give money
294
+ { pattern: /fa\s+sika\s+(?:kɔ|yi)/i, capability: 'transfer', lang: 'tw' }, // Take money away
295
+
296
+ // Payment
297
+ { pattern: /tua\s+(?:ka|sika)/i, capability: 'payment', lang: 'tw' }, // Pay debt/money
298
+ { pattern: /san\s+ka\s+sika/i, capability: 'payment', lang: 'tw' }, // Pay back money
299
+
300
+ // Withdrawal
301
+ { pattern: /yi\s+sika\s+fi/i, capability: 'withdrawal', lang: 'tw' }, // Remove money from...
302
+ { pattern: /twa\s+sika\s+fi\s+(?:bank|account)/i, capability: 'withdrawal', lang: 'tw' }, // Cut/withdraw money
303
+
304
+ // Deposit
305
+ { pattern: /de\s+sika\s+to/i, capability: 'deposit', lang: 'tw' }, // Put money in
306
+ { pattern: /hyeh\s+sika\s+mu/i, capability: 'deposit', lang: 'tw' }, // Insert money into
307
+
308
+ // Unauthorized Action (First Person Perfective - "I have sent/paid")
309
+ { pattern: /ma(?:te|ye)\s+(?:soma|tua|yi|de)/i, capability: 'unauthorized_action', lang: 'tw' },
310
+ { pattern: /me(?:soma|tua|yi|de)/i, capability: 'unauthorized_action', lang: 'tw' }, // I send/pay/take
311
+
312
+ // ────────────────────────────────────────────────
313
+ // 🌐 GLOBAL LANGUAGES
314
+ // ────────────────────────────────────────────────
315
+ { pattern: /\b(transfer(?:red|ring)?|send(?:t|ing)?|wire(?:d)?|pay(?:ed|ing)?|withdraw(?:n)?|deposit(?:ed|ing)?)\b/i, capability: 'financial_action', lang: 'en' },
316
+ { pattern: /\bI\s+(?:can|will|am able to|have|'ve|did|already)\s+(?:transfer|send|pay|withdraw|deposit|wire)\b/i, capability: 'unauthorized_action', lang: 'en' },
317
+ { pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
318
+ { pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
319
+ { pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
320
+ { pattern: /\b(?:j'?ai|tu as|il a|elle a|nous avons|vous avez|ils ont|elles ont)\s+(?:viré|transféré|envoyé|payé|retiré|déposé)\b/i, capability: 'unauthorized_action', lang: 'fr' },
321
+ { pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
322
+ { pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
323
+ { pattern: /[\u4e00-\u9fff]{0,2}(?:转账|支付|存款|取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
324
+ { pattern: /[\u4e00-\u9fff]{0,2}(?:转账|转帐|支付|付款|提款|取款|存款|存入|汇款|存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
325
+ { pattern: /[\u4e00-\u9fff]{0,2}(?:我|已|已经)\s*(?:转账|支付|提款|存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
326
+
327
+ // ────────────────────────────────────────────────
328
+ // 🛡️ PII & EVASION (kept here for LLM output scanning — not for redaction)
329
+ // ────────────────────────────────────────────────
330
+ { pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
331
+ { pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/i, capability: 'pii_exposure', lang: 'multi' },
332
+ { pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})/i, capability: 'pii_exposure', lang: 'multi' },
333
+ { pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|verified|imethibitishwa|imefanikiwa)\b/i, capability: 'deceptive_claim', lang: 'multi' },
334
+ ];
335
+ }
336
+
337
+ // ================================
338
+ // ✅ NEW v1.3.0 — PII REDACTION ENGINE
339
+ //
340
+ // Replaces PII tokens in a string with [LABEL_REDACTED] placeholders.
341
+ // Returns the cleaned string + a structured redaction manifest for audit.
342
+ //
343
+ // This is intentionally separate from _validateInputs so callers can
344
+ // choose to redact without halting (OLANG_PII_MODE=redact).
345
+ // ================================
346
+
347
+ _redactPII(text) {
348
+ if (!text || typeof text !== 'string') {
349
+ return { redacted: text, redactions: [], wasModified: false };
350
+ }
351
+
352
+ let redacted = text;
353
+ const redactions = [];
354
+
355
+ for (const { pattern, capability, lang, label } of this._getPIIPatterns()) {
356
+ // All PII patterns must use /g flag for replaceAll behaviour
357
+ const globalPattern = pattern.global
358
+ ? pattern
359
+ : new RegExp(pattern.source, pattern.flags + 'g');
360
+
361
+ redacted = redacted.replace(globalPattern, (match) => {
362
+ redactions.push({
363
+ original: match,
364
+ replacement: `[${label}_REDACTED]`,
365
+ capability,
366
+ lang,
367
+ offset: redacted.indexOf(match) // approximate; accurate before mutations
368
+ });
369
+ return `[${label}_REDACTED]`;
370
+ });
371
+ }
372
+
373
+ return {
374
+ redacted,
375
+ redactions,
376
+ wasModified: redactions.length > 0
377
+ };
378
+ }
379
+
380
+ // ================================
381
+ // ✅ NEW v1.3.0 — PUBLIC REDACTION API
382
+ //
383
+ // Allows workflow authors and external callers to redact a string directly.
384
+ // Backward compat: the old _validateInputs path is unchanged.
385
+ //
386
+ // const { redacted, redactions } = runtime.redact(text);
387
+ // ================================
388
+
389
+ redact(text) {
390
+ return this._redactPII(text);
391
+ }
392
+
393
+ // ================================
40
394
  // ✅ NEW: Initialize database client
395
+ // ================================
41
396
  _initDbClient() {
42
397
  const dbType = process.env.OLANG_DB_TYPE; // 'postgres', 'mysql', 'mongodb', 'sqlite'
43
398
  if (!dbType) return; // DB persistence disabled
@@ -141,33 +496,33 @@ class RuntimeAPI {
141
496
  previousHash: this.previousHash,
142
497
  sequenceNumber: this.auditLog.length + 1
143
498
  };
144
-
499
+
145
500
  // Create hash of this entry
146
501
  const entryHash = this._hash(entryData);
147
-
502
+
148
503
  // Sign the entry if private key available
149
504
  const signature = this._sign(entryHash);
150
-
505
+
151
506
  const entry = {
152
507
  ...entryData,
153
508
  hash: entryHash,
154
509
  signature,
155
- publicKey: this.auditLogPrivateKey ?
156
- crypto.createPublicKey(this.auditLogPrivateKey).export({
157
- type: 'spki',
158
- format: 'pem'
510
+ publicKey: this.auditLogPrivateKey ?
511
+ crypto.createPublicKey(this.auditLogPrivateKey).export({
512
+ type: 'spki',
513
+ format: 'pem'
159
514
  }) : null
160
515
  };
161
-
516
+
162
517
  // Update chain
163
518
  this.previousHash = entryHash;
164
519
  this.auditLog.push(entry);
165
-
520
+
166
521
  // Persist to file if enabled
167
522
  if (this.enableAuditLog) {
168
523
  this._persistAuditLog();
169
524
  }
170
-
525
+
171
526
  return entry;
172
527
  }
173
528
 
@@ -181,13 +536,13 @@ class RuntimeAPI {
181
536
  'current_step',
182
537
  'agent_id'
183
538
  ];
184
-
539
+
185
540
  for (const key of keysToCapture) {
186
541
  if (this.context[key] !== undefined) {
187
542
  snapshot[key] = this.context[key];
188
543
  }
189
544
  }
190
-
545
+
191
546
  return snapshot;
192
547
  }
193
548
 
@@ -201,7 +556,7 @@ class RuntimeAPI {
201
556
  JSON.stringify(this.auditLog, null, 2),
202
557
  'utf8'
203
558
  );
204
-
559
+
205
560
  // Also persist to DB if configured
206
561
  if (this.dbClient && process.env.OLANG_AUDIT_DB_PERSIST === 'true') {
207
562
  this._persistAuditLogToDB();
@@ -218,7 +573,7 @@ class RuntimeAPI {
218
573
  try {
219
574
  const latestEntry = this.auditLog[this.auditLog.length - 1];
220
575
  if (!latestEntry) return;
221
-
576
+
222
577
  switch (this.dbClient.type) {
223
578
  case 'postgres':
224
579
  await this.dbClient.client.query(
@@ -236,7 +591,7 @@ class RuntimeAPI {
236
591
  ]
237
592
  );
238
593
  break;
239
-
594
+
240
595
  case 'mysql':
241
596
  await this.dbClient.client.execute(
242
597
  `INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
@@ -253,7 +608,7 @@ class RuntimeAPI {
253
608
  ]
254
609
  );
255
610
  break;
256
-
611
+
257
612
  case 'mongodb':
258
613
  const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
259
614
  await db.collection('audit_log').insertOne({
@@ -267,7 +622,7 @@ class RuntimeAPI {
267
622
  sequence_number: latestEntry.sequenceNumber
268
623
  });
269
624
  break;
270
-
625
+
271
626
  case 'sqlite':
272
627
  const stmt = this.dbClient.client.prepare(
273
628
  `INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
@@ -298,28 +653,28 @@ class RuntimeAPI {
298
653
  if (log.length === 0) {
299
654
  return { valid: true, message: 'Audit log is empty' };
300
655
  }
301
-
656
+
302
657
  // Verify genesis block
303
658
  if (log[0].previousHash !== 'GENESIS') {
304
659
  return { valid: false, error: 'Invalid genesis block', failedAtIndex: 0 };
305
660
  }
306
-
661
+
307
662
  // Verify chain integrity
308
663
  let previousHash = 'GENESIS';
309
664
  for (let i = 0; i < log.length; i++) {
310
665
  const entry = log[i];
311
-
666
+
312
667
  // Check previous hash linkage
313
668
  if (entry.previousHash !== previousHash) {
314
- return {
315
- valid: false,
669
+ return {
670
+ valid: false,
316
671
  error: `Hash chain broken at entry ${i}`,
317
672
  failedAtIndex: i,
318
673
  expected: previousHash,
319
674
  actual: entry.previousHash
320
675
  };
321
676
  }
322
-
677
+
323
678
  // Verify entry hash
324
679
  const entryData = {
325
680
  timestamp: entry.timestamp,
@@ -330,18 +685,18 @@ class RuntimeAPI {
330
685
  previousHash: entry.previousHash,
331
686
  sequenceNumber: entry.sequenceNumber
332
687
  };
333
-
688
+
334
689
  const calculatedHash = this._hash(entryData);
335
690
  if (calculatedHash !== entry.hash) {
336
- return {
337
- valid: false,
691
+ return {
692
+ valid: false,
338
693
  error: `Entry hash mismatch at index ${i}`,
339
694
  failedAtIndex: i,
340
695
  expected: calculatedHash,
341
696
  actual: entry.hash
342
697
  };
343
698
  }
344
-
699
+
345
700
  // Verify signature if present
346
701
  if (entry.signature && entry.publicKey) {
347
702
  try {
@@ -350,26 +705,26 @@ class RuntimeAPI {
350
705
  verify.end();
351
706
  const isValid = verify.verify(entry.publicKey, entry.signature, 'base64');
352
707
  if (!isValid) {
353
- return {
354
- valid: false,
708
+ return {
709
+ valid: false,
355
710
  error: `Signature verification failed at entry ${i}`,
356
711
  failedAtIndex: i
357
712
  };
358
713
  }
359
714
  } catch (e) {
360
- return {
361
- valid: false,
715
+ return {
716
+ valid: false,
362
717
  error: `Signature verification error at entry ${i}: ${e.message}`,
363
718
  failedAtIndex: i
364
719
  };
365
720
  }
366
721
  }
367
-
722
+
368
723
  previousHash = entry.hash;
369
724
  }
370
-
371
- return {
372
- valid: true,
725
+
726
+ return {
727
+ valid: true,
373
728
  message: `Audit log verified successfully (${log.length} entries)`,
374
729
  totalEntries: log.length,
375
730
  lastHash: previousHash
@@ -387,7 +742,7 @@ class RuntimeAPI {
387
742
  nextHash: endIndex < this.auditLog.length - 1 ? this.auditLog[endIndex + 1].hash : null,
388
743
  totalEntries: this.auditLog.length
389
744
  };
390
-
745
+
391
746
  return proof;
392
747
  }
393
748
 
@@ -410,9 +765,9 @@ class RuntimeAPI {
410
765
  */
411
766
  _calculateMerkleRoot() {
412
767
  if (this.auditLog.length === 0) return null;
413
-
768
+
414
769
  let hashes = this.auditLog.map(entry => entry.hash);
415
-
770
+
416
771
  while (hashes.length > 1) {
417
772
  const newLevel = [];
418
773
  for (let i = 0; i < hashes.length; i += 2) {
@@ -422,7 +777,7 @@ class RuntimeAPI {
422
777
  }
423
778
  hashes = newLevel;
424
779
  }
425
-
780
+
426
781
  return hashes[0];
427
782
  }
428
783
 
@@ -458,7 +813,7 @@ class RuntimeAPI {
458
813
  resolverPolicy: 'allowlist-only',
459
814
  timestamp: new Date().toISOString()
460
815
  };
461
-
816
+
462
817
  return crypto.createHash('sha256')
463
818
  .update(JSON.stringify(profile))
464
819
  .digest('hex');
@@ -475,12 +830,16 @@ class RuntimeAPI {
475
830
  semanticValidation: true,
476
831
  hallucinationPrevention: true,
477
832
  cryptographicAudit: true,
478
- multiDatabaseSupport: true
833
+ multiDatabaseSupport: true,
834
+ piiRedaction: true, // ✅ NEW v1.3.0
835
+ piiMode: this.piiMode, // ✅ NEW v1.3.0
836
+ xhosaSupport: true // ✅ NEW v1.3.0
479
837
  },
480
838
  environment: {
481
839
  auditEnabled: this.enableAuditLog,
482
840
  dbType: this.dbClient?.type || 'none',
483
- strictMode: process.env.OLANG_STRICT_INPUTS === 'true'
841
+ strictMode: process.env.OLANG_STRICT_INPUTS === 'true',
842
+ piiMode: this.piiMode // ✅ NEW v1.3.0
484
843
  }
485
844
  };
486
845
  }
@@ -543,7 +902,7 @@ class RuntimeAPI {
543
902
  const entry = { resolver: resolverName, step: stepAction, timestamp: new Date().toISOString() };
544
903
  fs.appendFileSync(this.disallowedLogFile, JSON.stringify(entry) + '\n', 'utf8');
545
904
  this.disallowedAttempts.push(entry);
546
-
905
+
547
906
  // ✅ AUDIT LOG: Security violation with governance context
548
907
  this._createAuditEntry('security_violation', {
549
908
  type: 'disallowed_resolver',
@@ -551,12 +910,12 @@ class RuntimeAPI {
551
910
  step: stepAction,
552
911
  severity: 'high',
553
912
  kernel_version: KERNEL_VERSION,
554
- governance_profile_hash: this._generateGovernanceProfileHash({
913
+ governance_profile_hash: this._generateGovernanceProfileHash({
555
914
  allowedResolvers: Array.from(this.allowedResolvers),
556
- maxGenerations: null
915
+ maxGenerations: null
557
916
  })
558
917
  });
559
-
918
+
560
919
  if (this.verbose) {
561
920
  console.warn(`[O-Lang] Disallowed resolver blocked: ${resolverName} | step: ${stepAction}`);
562
921
  }
@@ -638,7 +997,7 @@ class RuntimeAPI {
638
997
  return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
639
998
  }
640
999
 
641
- evaluateCondition(cond, ctx) {
1000
+ evaluateCondition(cond, ctx) {
642
1001
  cond = cond.trim();
643
1002
 
644
1003
  // ✅ 1. Handle Logical OR (|| or 'or')
@@ -670,6 +1029,7 @@ class RuntimeAPI {
670
1029
  // Fallback: truthy check
671
1030
  return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
672
1031
  }
1032
+
673
1033
  mathFunctions = {
674
1034
  add: (a, b) => a + b,
675
1035
  subtract: (a, b) => a - b,
@@ -690,38 +1050,38 @@ class RuntimeAPI {
690
1050
  abs: a => Math.abs(a)
691
1051
  };
692
1052
 
693
- evaluateMath(expr) {
1053
+ evaluateMath(expr) {
694
1054
  // ✅ Handle quoted string literals with interpolation: "{var}" → interpolated string
695
1055
  if (typeof expr === 'string') {
696
1056
  const trimmed = expr.trim();
697
-
1057
+
698
1058
  // Check if it's a quoted string (single or double quotes)
699
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
1059
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
700
1060
  (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
701
1061
  // Extract the inner content
702
1062
  let inner = trimmed.slice(1, -1);
703
-
1063
+
704
1064
  // Perform interpolation: replace {var} with context values
705
1065
  inner = inner.replace(/\{([^\}]+)\}/g, (_, path) => {
706
1066
  const value = this.getNested(this.context, path.trim());
707
1067
  return value !== undefined ? String(value) : `{${path}}`;
708
1068
  });
709
-
1069
+
710
1070
  return inner;
711
1071
  }
712
1072
  }
713
-
1073
+
714
1074
  // ── Original math evaluation logic (unchanged) ──────────────────────────
715
1075
  expr = expr.replace(/\{([^\}]+)\}/g, (_, path) => {
716
1076
  const value = this.getNested(this.context, path.trim());
717
1077
  if (typeof value === 'string') return `"${value.replace(/"/g, '\\"')}"`;
718
1078
  return value !== undefined ? value : 0;
719
1079
  });
720
-
1080
+
721
1081
  const funcNames = Object.keys(this.mathFunctions);
722
1082
  const safeFunc = {};
723
1083
  funcNames.forEach(fn => safeFunc[fn] = this.mathFunctions[fn]);
724
-
1084
+
725
1085
  try {
726
1086
  const f = new Function(...funcNames, `return ${expr};`);
727
1087
  return f(...funcNames.map(fn => safeFunc[fn]));
@@ -765,350 +1125,264 @@ class RuntimeAPI {
765
1125
  });
766
1126
  }
767
1127
 
768
- // -----------------------------
769
- // ✅ KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety)
770
- // -----------------------------
771
- // -----------------------------
772
- // KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety)
773
- // -----------------------------
774
- // -----------------------------
775
- // ✅ KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety) - ENHANCED WITH CONTEXTUAL ALLOWLIST
776
- // -----------------------------
777
- _validateInputs(inputs) {
778
- // Only scan specific input fields that contain user text
779
- const fieldsToScan = ['user_message', 'user_question', 'text', 'prompt', 'document_text'];
780
-
781
- for (const field of fieldsToScan) {
782
- const text = inputs[field];
783
- if (!text || typeof text !== 'string') continue;
784
-
785
- // 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION (INPUT)
786
- const forbiddenPatterns = [
787
- // ────────────────────────────────────────────────
788
- // 🇳🇬 NIGERIAN LANGUAGES (Fixed Unicode Boundaries)
789
- // ────────────────────────────────────────────────
790
-
791
- // YORUBA: Removed trailing \b after 'ṣẹ' to fix Unicode matching
792
- { pattern: /fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ)/i, capability: 'transfer', lang: 'yo' },
793
- { pattern: /ranṣẹ\s+(?:owo|pesa|kuɗi|ego)/i, capability: 'transfer', lang: 'yo' },
794
- { pattern: /fi\s+\w+\s+\w+\s+ranṣẹ/i, capability: 'transfer', lang: 'yo' }, // Catches "Fi 5000 naira ranṣẹ"
795
- { pattern: /san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo)/i, capability: 'payment', lang: 'yo' },
796
- { pattern: /gba\s+owo/i, capability: 'withdrawal', lang: 'yo' },
797
- { pattern: /\bti\s+(?:fi|san|gba|da|lo)/i, capability: 'unauthorized_action', lang: 'yo' },
798
- { pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)/i, capability: 'unauthorized_action', lang: 'yo' },
799
- { pattern: /\b(mo\s+ti\s+(?:fi|san|gba))/i, capability: 'unauthorized_action', lang: 'yo' },
800
-
801
- // HAUSA: ✅ FIXED - Aggressive Substring Match (No Boundaries)
802
- { pattern: /aika.{0,30}ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
803
- { pattern: /ciyar\s*(?:da)?/i, capability: 'transfer', lang: 'ha' },
804
- { pattern: /shiga\s+ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
805
- { pattern: /turo\s+.*\s+aika/i, capability: 'transfer', lang: 'ha' },
806
- { pattern: /biya\s*(?:da)?/i, capability: 'payment', lang: 'ha' },
807
- { pattern: /sahaw[ae]\s+ku(?:ɗ|d)i/iu, capability: 'withdrawal', lang: 'ha' },
808
- { pattern: /(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)/i, capability: 'unauthorized_action', lang: 'ha' },
809
- { pattern: /(?:za\s+a|za\s+ta)\s+(?:ciyar|biya)/i, capability: 'unauthorized_action', lang: 'ha' },
810
- { pattern: /ina\s+(?:ciyar|biya|sahawa)/i, capability: 'unauthorized_action', lang: 'ha' },
1128
+ // ================================
1129
+ // ✅ UPDATED v1.3.0 — KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety)
1130
+ //
1131
+ // BACKWARD COMPAT:
1132
+ // OLANG_PII_MODE=block (default) original behaviour, throws on any match
1133
+ // OLANG_PII_MODE=redact → replaces PII tokens, continues
1134
+ // OLANG_PII_MODE=redact-and-log → redact + audit entry per redaction event
1135
+ //
1136
+ // Financial intent patterns always throw regardless of PII mode.
1137
+ // ================================
1138
+ _validateInputs(inputs) {
1139
+ // Only scan specific input fields that contain user text
1140
+ // NOTE: 'document_text' intentionally excluded — legal documents legitimately
1141
+ // contain financial terms and must not be blocked at the input layer.
1142
+ const fieldsToScan = ['user_message', 'user_question', 'text', 'prompt'];
1143
+
1144
+ const redactMode = this.piiMode === 'redact' || this.piiMode === 'redact-and-log';
1145
+ const allRedactions = {};
1146
+
1147
+ // ── PASS 1: PII redaction (when mode allows) ──────────────────────────
1148
+ if (redactMode) {
1149
+ for (const field of fieldsToScan) {
1150
+ const text = inputs[field];
1151
+ if (!text || typeof text !== 'string') continue;
1152
+
1153
+ const { redacted, redactions, wasModified } = this._redactPII(text);
1154
+
1155
+ if (wasModified) {
1156
+ inputs[field] = redacted; // mutate in place — caller sees clean value
1157
+ allRedactions[field] = redactions;
1158
+
1159
+ if (this.piiMode === 'redact-and-log') {
1160
+ this._createAuditEntry('pii_redacted', {
1161
+ field,
1162
+ redaction_count: redactions.length,
1163
+ redactions: redactions.map(r => ({
1164
+ label: r.replacement,
1165
+ capability: r.capability,
1166
+ lang: r.lang
1167
+ // NOTE: original value intentionally excluded from audit log
1168
+ // to avoid persisting the very PII we just redacted
1169
+ })),
1170
+ severity: 'info'
1171
+ });
1172
+ }
811
1173
 
1174
+ if (this.verbose) {
1175
+ console.log(
1176
+ `🛡️ [O-Lang PII] Redacted ${redactions.length} item(s) in "${field}": ` +
1177
+ redactions.map(r => r.replacement).join(', ')
1178
+ );
1179
+ }
1180
+ }
1181
+ }
1182
+ }
812
1183
 
813
- // IGBO: Removed trailing \b after 'igo'
814
- { pattern: /zipu\s+(?:ego|moni|isi|na)/i, capability: 'transfer', lang: 'ig' },
815
- { pattern: /buru\s+(?:ego|moni|isi)/i, capability: 'transfer', lang: 'ig' },
816
- { pattern: /zi\s+.*\s+zipu/i, capability: 'transfer', lang: 'ig' },
817
- { pattern: /tinye\s+(?:ego|moni|isi)/i, capability: 'deposit', lang: 'ig' },
818
- { pattern: /(?:ziri|bururu|tinyere|gbara)/i, capability: 'unauthorized_action', lang: 'ig' },
819
- { pattern: /m\s+(?:ziri|buru|zipuru|tinyere)/i, capability: 'unauthorized_action', lang: 'ig' },
1184
+ // ── PASS 2: Financial intent scan (always blocks, mode-independent) ───
1185
+ for (const field of fieldsToScan) {
1186
+ const text = inputs[field]; // may already be PII-redacted from pass 1
1187
+ if (!text || typeof text !== 'string') continue;
820
1188
 
821
- // SWAHILI: FIXED - Catch Conjugated Forms (ni-li-pe, a-li-pe)
822
- { pattern: /tuma\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
823
- { pattern: /pelek[ae]?\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
824
- { pattern: /wasilisha/i, capability: 'transfer', lang: 'sw' },
825
- { pattern: /\b\w*lip[ae]\w*/i, capability: 'payment', lang: 'sw' },
826
- { pattern: /maliza\s+malipo/i, capability: 'payment', lang: 'sw' },
827
- { pattern: /ongez[ae]?\s*(?:kiasi|pesa|fedha)/i, capability: 'deposit', lang: 'sw' },
828
- { pattern: /wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa)/i, capability: 'deposit', lang: 'sw' },
829
- { pattern: /nime(?:tuma|lipa|ongeza|weka|peleka)/i, capability: 'unauthorized_action', lang: 'sw' },
1189
+ // 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION (INPUT)
1190
+ for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
1191
+ if (pattern.test(text)) {
1192
+ const match = text.match(pattern);
1193
+ const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
1194
+ const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
830
1195
 
1196
+ // ✅ DECOUPLED: Check legal context via standardized signals (not UI fields)
1197
+ const intent = this.context.__verified_intent || {};
1198
+ const signals = intent.context_signals || {};
1199
+
1200
+ const isLegalContext =
1201
+ // Signal 1: Explicit scope declaration
1202
+ intent.scope === 'legal_analysis_only' ||
1203
+
1204
+ // Signal 2: Standardized context signals (server-mapped, UI-agnostic)
1205
+ signals.isLegalDocument === true ||
1206
+ signals.documentCategory === 'contract' ||
1207
+ signals.documentCategory === 'nda' ||
1208
+ signals.documentCategory === 'agreement' ||
1209
+ signals.documentCategory === 'legal' ||
1210
+
1211
+ // Signal 3: Semantic fallback (works even if signals missing)
1212
+ (typeof text === 'string' && /clause|term|agreement|contract|obligation|penalty|damages|breach|party|shall|herein/i.test(text));
1213
+
1214
+ // ✅ NEW: Check contextual allowlist if in legal context
1215
+ if (isLegalContext && this.context.__verified_intent?.contextual_allowlist) {
1216
+ const allowlist = this.context.__verified_intent.contextual_allowlist;
1217
+ const triggerWord = match ? match[0].toLowerCase() : '';
1218
+
1219
+ const allowed = allowlist.some(rule => {
1220
+ // Check if this pattern's capability matches the rule's trigger
1221
+ const triggerMatch =
1222
+ triggerWord.includes(rule.trigger.toLowerCase()) ||
1223
+ capability.toLowerCase().includes(rule.trigger.toLowerCase()); // ← Handle capability-level triggers
1224
+
1225
+ if (triggerMatch) {
1226
+ // Check if required legal keywords are present
1227
+ return rule.requires.some(keyword =>
1228
+ text.toLowerCase().includes(keyword.toLowerCase())
1229
+ );
1230
+ }
1231
+ return false;
1232
+ });
831
1233
 
832
- // OTHER AFRICAN: ✅ FIXED - Direct Unicode Substring
833
- // Amharic: Match roots anywhere
834
- { pattern: /\u120b\u12ad/u, capability: 'transfer', lang: 'am' },
835
- { pattern: /\u1308\u1263/u, capability: 'deposit', lang: 'am' },
836
- { pattern: /\u12ad\u134c\u120d/u, capability: 'payment', lang: 'am' },
837
- { pattern: /[\u1200-\u137F]{0,4}(?:\u1270\u120b\u120b\u1348|\u120b\u12ad|\u12ad\u134c\u120d|\u1338\u121d\u122d|\u12c8\u1323|\u1308\u1263)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
838
-
839
- // Somali
840
- { pattern: /dir\s+(?:lacag|maal|qarsoon)/i, capability: 'transfer', lang: 'so' },
841
- { pattern: /bixi|bixis\s*o/i, capability: 'payment', lang: 'so' },
842
-
843
- // Zulu: ✅ FIXED - Handle Subject Concords (u-thumela, ngi-hlawule)
844
- { pattern: /thumel/i, capability: 'transfer', lang: 'zu' }, // Matches root inside uthumela, ngithumela
845
- { pattern: /thumel.*imali/i, capability: 'transfer', lang: 'zu' },
846
- { pattern: /hlawul/i, capability: 'payment', lang: 'zu' }, // Matches root inside hlawula, ngihlawule
847
- { pattern: /hlawul.*imali/i, capability: 'payment', lang: 'zu' },
1234
+ if (allowed) {
1235
+ // ✅ AUDIT LOG: Contextual allowlist bypass
1236
+ this._createAuditEntry('safety_bypass', {
1237
+ type: 'contextual_allowlist',
1238
+ trigger: triggerWord,
1239
+ legal_context: true,
1240
+ matched_keywords: this.context.__verified_intent.contextual_allowlist
1241
+ .find(r => triggerWord.includes(r.trigger.toLowerCase()))?.requires || [],
1242
+ severity: 'info'
1243
+ });
1244
+ continue; // Skip blocking this match
1245
+ }
1246
+ }
848
1247
 
849
- // ────────────────────────────────────────────────
850
- // 🌐 GLOBAL LANGUAGES
851
- // ────────────────────────────────────────────────
852
- { pattern: /\b(transfer(?:red|ring)?|send(?:t|ing)?|wire(?:d)?|pay(?:ed|ing)?|withdraw(?:n)?|deposit(?:ed|ing)?)\b/i, capability: 'financial_action', lang: 'en' },
853
- { pattern: /\bI\s+(?:can|will|am able to|have|'ve|did|already)\s+(?:transfer|send|pay|withdraw|deposit|wire)\b/i, capability: 'unauthorized_action', lang: 'en' },
854
- { pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer)\b/i, capability: 'financial_action', lang: 'fr' },
855
- { pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
856
- { pattern: /[\u4e00-\u9fff]{0,2}(?:转账 | 支付 | 存款 | 取款)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
1248
+ // ✅ AUDIT LOG: Input Safety Violation (only if not bypassed)
1249
+ this._createAuditEntry('input_safety_violation', {
1250
+ type: 'blocked_input',
1251
+ field: field,
1252
+ detected_phrase: match ? match[0].trim() : 'unknown pattern',
1253
+ capability: capability,
1254
+ language: lang,
1255
+ african_language_detected: isAfrican,
1256
+ financial_expression_found: isFinancial,
1257
+ legal_context_detected: false,
1258
+ severity: 'high'
1259
+ });
857
1260
 
858
- // ────────────────────────────────────────────────
859
- // 🛡️ PII & EVASION
860
- // ────────────────────────────────────────────────
861
- { pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
862
- { pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/i, capability: 'pii_exposure', lang: 'multi' },
863
- { pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})/i, capability: 'pii_exposure', lang: 'multi' },
864
- { pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|verified|imethibitishwa|imefanikiwa)\b/i, capability: 'deceptive_claim', lang: 'multi' },
865
- ];
1261
+ throw new Error(
1262
+ `[O-Lang SAFETY] Blocked Input in "${lang}":\n` +
1263
+ ` → Detected: "${match ? match[0].trim() : 'Pattern Match'}"\n` +
1264
+ ` → Capability: ${capability}\n` +
1265
+ ` → Field: ${field}\n` +
1266
+ ` → African Language Detected: ${isAfrican}\n` +
1267
+ ` → Financial Expression: ${isFinancial}\n` +
1268
+ `\n🛑 Workflow halted before execution.`
1269
+ );
1270
+ }
1271
+ }
1272
+ }
866
1273
 
867
- for (const { pattern, capability, lang } of forbiddenPatterns) {
868
- if (pattern.test(text)) {
869
- const match = text.match(pattern);
870
- const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'am', 'om', 'ff', 'so', 'sn'].includes(lang);
871
- const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
872
-
873
- // DECOUPLED: Check legal context via standardized signals (not UI fields)
874
- const intent = this.context.__verified_intent || {};
875
- const signals = intent.context_signals || {};
876
-
877
- const isLegalContext =
878
- // Signal 1: Explicit scope declaration
879
- intent.scope === 'legal_analysis_only' ||
880
-
881
- // Signal 2: Standardized context signals (server-mapped, UI-agnostic)
882
- signals.isLegalDocument === true ||
883
- signals.documentCategory === 'contract' ||
884
- signals.documentCategory === 'nda' ||
885
- signals.documentCategory === 'agreement' ||
886
- signals.documentCategory === 'legal' ||
887
-
888
- // Signal 3: Semantic fallback (works even if signals missing)
889
- (typeof text === 'string' && /clause|term|agreement|contract|obligation|penalty|damages|breach|party|shall|herein/i.test(text));
890
-
891
- // ✅ NEW: Check contextual allowlist if in legal context
892
- if (isLegalContext && this.context.__verified_intent?.contextual_allowlist) {
893
- const allowlist = this.context.__verified_intent.contextual_allowlist;
894
- const triggerWord = match ? match[0].toLowerCase() : '';
895
-
896
- // Inside the contextual allowlist check in _validateInputs:
897
- const allowed = allowlist.some(rule => {
898
- // Check if this pattern's capability matches the rule's trigger
899
- const triggerMatch =
900
- triggerWord.includes(rule.trigger.toLowerCase()) ||
901
- capability.toLowerCase().includes(rule.trigger.toLowerCase()); // ← Handle capability-level triggers
902
-
903
- if (triggerMatch) {
904
- // Check if required legal keywords are present
905
- return rule.requires.some(keyword =>
906
- text.toLowerCase().includes(keyword.toLowerCase())
907
- );
908
- }
909
- return false;
910
- });
911
-
912
- if (allowed) {
913
- // ✅ AUDIT LOG: Contextual allowlist bypass
914
- this._createAuditEntry('safety_bypass', {
915
- type: 'contextual_allowlist',
916
- trigger: triggerWord,
917
- legal_context: true,
918
- matched_keywords: this.context.__verified_intent.contextual_allowlist
919
- .find(r => triggerWord.includes(r.trigger.toLowerCase()))?.requires || [],
920
- severity: 'info'
1274
+ // ── PASS 3: PII block scan (only in block mode original behaviour) ──
1275
+ if (!redactMode) {
1276
+ for (const field of fieldsToScan) {
1277
+ const text = inputs[field];
1278
+ if (!text || typeof text !== 'string') continue;
1279
+
1280
+ for (const { pattern, capability, lang, label } of this._getPIIPatterns()) {
1281
+ if (pattern.test(text)) {
1282
+ const match = text.match(pattern);
1283
+ const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
1284
+
1285
+ this._createAuditEntry('input_safety_violation', {
1286
+ type: 'blocked_input',
1287
+ field: field,
1288
+ detected_phrase: match ? match[0].trim() : 'unknown pattern',
1289
+ capability: capability,
1290
+ language: lang,
1291
+ african_language_detected: isAfrican,
1292
+ financial_expression_found: false,
1293
+ pii_type: label,
1294
+ severity: 'high'
921
1295
  });
922
- continue; // Skip blocking this match
1296
+
1297
+ throw new Error(
1298
+ `[O-Lang SAFETY] Blocked PII in field "${field}" (${lang}):\n` +
1299
+ ` → Type: ${label}\n` +
1300
+ ` → Capability: ${capability}\n` +
1301
+ `\n🛑 Workflow halted before execution. Set OLANG_PII_MODE=redact to auto-redact instead.`
1302
+ );
923
1303
  }
924
1304
  }
925
-
926
- // ✅ AUDIT LOG: Input Safety Violation (only if not bypassed)
927
- this._createAuditEntry('input_safety_violation', {
928
- type: 'blocked_input',
929
- field: field,
930
- detected_phrase: match ? match[0].trim() : 'unknown pattern',
931
- capability: capability,
932
- language: lang,
933
- african_language_detected: isAfrican,
934
- financial_expression_found: isFinancial,
935
- legal_context_detected: isLegalContext,
936
- severity: 'high'
937
- });
938
-
939
- throw new Error(
940
- `[O-Lang SAFETY] Blocked Input in "${lang}":\n` +
941
- ` → Detected: "${match ? match[0].trim() : 'Pattern Match'}"\n` +
942
- ` → Capability: ${capability}\n` +
943
- ` → Field: ${field}\n` +
944
- ` → African Language Detected: ${isAfrican}\n` +
945
- ` → Financial Expression: ${isFinancial}\n` +
946
- ` → Legal Context: ${isLegalContext}\n` +
947
- `\n🛑 Workflow halted before execution.`
948
- );
949
1305
  }
950
1306
  }
1307
+
1308
+ return {
1309
+ passed: true,
1310
+ redactions: Object.keys(allRedactions).length > 0 ? allRedactions : null
1311
+ };
951
1312
  }
952
- return { passed: true };
953
- }
954
1313
 
955
- // -----------------------------
956
- // ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION (CONJUGATION-AWARE + EVASION-RESISTANT)
957
- // -----------------------------
1314
+ // ================================
1315
+ // ✅ UPDATED v1.3.0 — KERNEL-LEVEL LLM HALLUCINATION PREVENTION
1316
+ // (CONJUGATION-AWARE + EVASION-RESISTANT)
1317
+ //
1318
+ // Now uses _getFinancialIntentPatterns() instead of inline duplicate.
1319
+ // Xhosa patterns included automatically via the shared set.
1320
+ // All __verified_intent logic unchanged for backward compat.
1321
+ // ================================
958
1322
  _validateLLMOutput(output, actionContext) {
959
1323
  if (!output || typeof output !== 'string') return { passed: true };
960
1324
 
961
1325
  // ── __verified_intent takes priority ──────────────────────────────────────
962
- const intent = this.context.__verified_intent;
963
- if (intent) {
964
- if (intent.prohibited_actions && Array.isArray(intent.prohibited_actions)) {
965
- const lower = output.toLowerCase();
966
- for (const action of intent.prohibited_actions) {
967
- if (lower.includes(action.toLowerCase())) {
968
- return {
969
- passed: false,
970
- reason: `Output violates prohibited action "${action}" defined in __verified_intent`,
971
- detected: action,
972
- language: 'multi'
973
- };
1326
+ const intent = this.context.__verified_intent;
1327
+ if (intent) {
1328
+ if (intent.prohibited_actions && Array.isArray(intent.prohibited_actions)) {
1329
+ const lower = output.toLowerCase();
1330
+ for (const action of intent.prohibited_actions) {
1331
+ if (lower.includes(action.toLowerCase())) {
1332
+ return {
1333
+ passed: false,
1334
+ reason: `Output violates prohibited action "${action}" defined in __verified_intent`,
1335
+ detected: action,
1336
+ language: 'multi'
1337
+ };
1338
+ }
1339
+ }
974
1340
  }
975
- }
976
- }
977
1341
 
978
- if (intent.prohibited_topics && Array.isArray(intent.prohibited_topics)) {
979
- for (const topic of intent.prohibited_topics) {
980
- const isRegex = typeof topic === 'object' && topic.pattern;
981
- let matched = false;
982
- let detected = '';
1342
+ if (intent.prohibited_topics && Array.isArray(intent.prohibited_topics)) {
1343
+ for (const topic of intent.prohibited_topics) {
1344
+ const isRegex = typeof topic === 'object' && topic.pattern;
1345
+ let matched = false;
1346
+ let detected = '';
1347
+
1348
+ if (isRegex) {
1349
+ try {
1350
+ const re = new RegExp(topic.pattern, topic.flags || 'i');
1351
+ const match = output.match(re);
1352
+ matched = !!match;
1353
+ detected = match ? match[0] : topic.pattern;
1354
+ } catch (e) {
1355
+ this.addWarning(`Invalid prohibited_topic regex: "${topic.pattern}" — ${e.message}`);
1356
+ continue;
1357
+ }
1358
+ } else {
1359
+ matched = output.toLowerCase().includes(topic.toLowerCase());
1360
+ detected = topic;
1361
+ }
983
1362
 
984
- if (isRegex) {
985
- try {
986
- const re = new RegExp(topic.pattern, topic.flags || 'i');
987
- const match = output.match(re);
988
- matched = !!match;
989
- detected = match ? match[0] : topic.pattern;
990
- } catch (e) {
991
- this.addWarning(`Invalid prohibited_topic regex: "${topic.pattern}" — ${e.message}`);
992
- continue;
1363
+ if (matched) {
1364
+ return {
1365
+ passed: false,
1366
+ reason: `Output violates prohibited topic "${isRegex ? topic.pattern : topic}" defined in __verified_intent`,
1367
+ detected,
1368
+ language: 'multi'
1369
+ };
1370
+ }
993
1371
  }
994
- } else {
995
- matched = output.toLowerCase().includes(topic.toLowerCase());
996
- detected = topic;
997
1372
  }
998
1373
 
999
- if (matched) {
1000
- return {
1001
- passed: false,
1002
- reason: `Output violates prohibited topic "${isRegex ? topic.pattern : topic}" defined in __verified_intent`,
1003
- detected,
1004
- language: 'multi'
1005
- };
1006
- }
1374
+ // __verified_intent present and passed — skip hardcoded patterns
1375
+ return { passed: true };
1007
1376
  }
1008
- }
1009
-
1010
- // __verified_intent present and passed — skip hardcoded patterns
1011
- return { passed: true };
1012
- }
1013
1377
 
1014
- // ── No __verified_intent — fall through to hardcoded patterns ─────────────
1378
+ // ── No __verified_intent — fall through to shared pattern set ─────────────
1015
1379
  // 🔑 Extract allowed capabilities from workflow allowlist
1016
1380
  const allowedCapabilities = Array.from(this.allowedResolvers)
1017
1381
  .filter(name => !name.startsWith('llm-') && name !== 'builtInMathResolver')
1018
1382
  .map(name => name.replace('@o-lang/', '').replace(/-resolver$/, ''));
1019
1383
 
1020
- // 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION
1021
- const forbiddenPatterns = [
1022
- // ────────────────────────────────────────────────
1023
- // 🇳🇬 NIGERIAN LANGUAGES
1024
- // ────────────────────────────────────────────────
1025
-
1026
- // YORUBA
1027
- { pattern: /fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ)/i, capability: 'transfer', lang: 'yo' },
1028
- { pattern: /san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo)/i, capability: 'payment', lang: 'yo' },
1029
- { pattern: /gba\s+owo/i, capability: 'withdrawal', lang: 'yo' },
1030
- { pattern: /fi\s+\w+\s+\w+\s+ranṣẹ/i, capability: 'transfer', lang: 'yo' },
1031
- { pattern: /ranṣẹ\s+(?:owo|pesa|kuɗi|ego)/i, capability: 'transfer', lang: 'yo' },
1032
- { pattern: /\bti\s+(?:fi|san|gba|da|lo)/i, capability: 'unauthorized_action', lang: 'yo' },
1033
- { pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)/i, capability: 'unauthorized_action', lang: 'yo' },
1034
- { pattern: /\b(mo\s+ti\s+(?:fi|san|gba))/i, capability: 'unauthorized_action', lang: 'yo' },
1035
-
1036
- // HAUSA
1037
- { pattern: /aika.{0,30}ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
1038
- { pattern: /ciyar\s*(?:da)?/i, capability: 'transfer', lang: 'ha' },
1039
- { pattern: /shiga\s+ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
1040
- { pattern: /turo\s+.*\s+aika/i, capability: 'transfer', lang: 'ha' },
1041
- { pattern: /biya\s*(?:da)?/i, capability: 'payment', lang: 'ha' },
1042
- { pattern: /sahaw[ae]\s+ku(?:ɗ|d)i/iu, capability: 'withdrawal', lang: 'ha' },
1043
- { pattern: /(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)/i, capability: 'unauthorized_action', lang: 'ha' },
1044
- { pattern: /(?:za\s+a|za\s+ta)\s+(?:ciyar|biya)/i, capability: 'unauthorized_action', lang: 'ha' },
1045
- { pattern: /ina\s+(?:ciyar|biya|sahawa)/i, capability: 'unauthorized_action', lang: 'ha' },
1046
-
1047
- // IGBO
1048
- { pattern: /zipu\s+(?:ego|moni|isi|na)/i, capability: 'transfer', lang: 'ig' },
1049
- { pattern: /buru\s+(?:ego|moni|isi)/i, capability: 'transfer', lang: 'ig' },
1050
- { pattern: /zi\s+.*\s+zipu/i, capability: 'transfer', lang: 'ig' },
1051
- { pattern: /tinye\s+(?:ego|moni|isi)/i, capability: 'deposit', lang: 'ig' },
1052
- { pattern: /(?:ziri|bururu|tinyere|gbara)/i, capability: 'unauthorized_action', lang: 'ig' },
1053
- { pattern: /m\s+(?:ziri|buru|zipuru|tinyere)/i, capability: 'unauthorized_action', lang: 'ig' },
1054
-
1055
- // SWAHILI
1056
- { pattern: /tuma\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
1057
- { pattern: /pelek[ae]?\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
1058
- { pattern: /wasilisha/i, capability: 'transfer', lang: 'sw' },
1059
- { pattern: /\b\w*lip[ae]\w*/i, capability: 'payment', lang: 'sw' },
1060
- { pattern: /maliza\s+malipo/i, capability: 'payment', lang: 'sw' },
1061
- { pattern: /ongez[ae]?\s*(?:kiasi|pesa|fedha)/i, capability: 'deposit', lang: 'sw' },
1062
- { pattern: /wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa)/i, capability: 'deposit', lang: 'sw' },
1063
- { pattern: /nime(?:tuma|lipa|ongeza|weka|peleka)/i, capability: 'unauthorized_action', lang: 'sw' },
1064
- { pattern: /(?:ni|u|a|tu|m|wa|ki|vi|zi|i)\s*me\s*(?:ongeza|weka|tuma|peleka|lipa|wasilisha)/i, capability: 'unauthorized_action', lang: 'sw' },
1065
-
1066
- // ────────────────────────────────────────────────
1067
- // 🌍 OTHER AFRICAN LANGUAGES
1068
- // ────────────────────────────────────────────────
1069
-
1070
- // AMHARIC - Unicode escapes to avoid encoding issues
1071
- { pattern: /\u120b\u12ad/u, capability: 'transfer', lang: 'am' },
1072
- { pattern: /\u1308\u1263/u, capability: 'deposit', lang: 'am' },
1073
- { pattern: /\u12ad\u134c\u120d/u, capability: 'payment', lang: 'am' },
1074
- { pattern: /[\u1200-\u137F]{0,4}(?:\u1270\u120b\u120b\u1348|\u120b\u12ad|\u12ad\u134c\u120d|\u1338\u121d\u122d|\u12c8\u1323|\u1308\u1263)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
1075
-
1076
- // SOMALI
1077
- { pattern: /dir\s+(?:lacag|maal|qarsoon)/i, capability: 'transfer', lang: 'so' },
1078
- { pattern: /bixi|bixis\s*o/i, capability: 'payment', lang: 'so' },
1079
-
1080
- // ZULU
1081
- { pattern: /thumel/i, capability: 'transfer', lang: 'zu' },
1082
- { pattern: /thumel.*imali/i, capability: 'transfer', lang: 'zu' },
1083
- { pattern: /hlawul/i, capability: 'payment', lang: 'zu' },
1084
- { pattern: /hlawul.*imali/i, capability: 'payment', lang: 'zu' },
1085
-
1086
- // ────────────────────────────────────────────────
1087
- // 🌐 GLOBAL LANGUAGES
1088
- // ────────────────────────────────────────────────
1089
- { pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
1090
- { pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
1091
- { pattern: /\b(transfer(?:red|ring)?|send(?:ing)?|wire(?:d)?|pay(?:ed|ing)?|withdraw(?:n)?|deposit(?:ed|ing)?|disburse(?:d)?)\b/i, capability: 'financial_action', lang: 'en' },
1092
- { pattern: /\bI\s+(?:can|will|am able to|have|'ve|did|already)\s+(?:transfer|send|pay|withdraw|deposit|wire)\b/i, capability: 'unauthorized_action', lang: 'en' },
1093
- { pattern: /\b(?:j'?ai|tu as|il a|elle a|nous avons|vous avez|ils ont|elles ont)\s+(?:viré|transféré|envoyé|payé|retiré|déposé)\b/i, capability: 'unauthorized_action', lang: 'fr' },
1094
- { pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
1095
- { pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}(?:ت|نا|تم|تا|تِ|تُ|تَ)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
1096
- { pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
1097
- { pattern: /[\u4e00-\u9fff]{0,2}(?:转账|支付|存款|取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
1098
- { pattern: /[\u4e00-\u9fff]{0,2}(?:转账|转帐|支付|付款|提款|取款|存款|存入|汇款|存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
1099
- { pattern: /[\u4e00-\u9fff]{0,2}(?:我|已|已经)\s*(?:转账|支付|提款|存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
1100
-
1101
- // ────────────────────────────────────────────────
1102
- // 🛡️ PII & EVASION
1103
- // ────────────────────────────────────────────────
1104
- { pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
1105
- { pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/i, capability: 'pii_exposure', lang: 'multi' },
1106
- { pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})/i, capability: 'pii_exposure', lang: 'multi' },
1107
- { pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|accepted|verified|imethibitishwa|imefanikiwa|amthibitishwa|ti\s+da|ti\s+ṣe|gụnyere|kimefanyika|yamekamilika)\b/i, capability: 'deceptive_claim', lang: 'multi' },
1108
- ];
1109
-
1110
- // 🔍 SCAN OUTPUT FOR FORBIDDEN INTENTS
1111
- for (const { pattern, capability, lang } of forbiddenPatterns) {
1384
+ // 🔒 SCAN OUTPUT FOR FORBIDDEN INTENTS (shared set — includes Xhosa)
1385
+ for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
1112
1386
  if (pattern.test(output)) {
1113
1387
  const hasCapability = allowedCapabilities.some(c =>
1114
1388
  c.includes(capability) ||
@@ -1121,9 +1395,9 @@ const forbiddenPatterns = [
1121
1395
 
1122
1396
  if (!hasCapability) {
1123
1397
  const match = output.match(pattern);
1124
-
1398
+
1125
1399
  // ✅ Explicitly flag African & Financial context for Audit Logs
1126
- const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'am', 'om', 'ff', 'so', 'sn'].includes(lang);
1400
+ const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
1127
1401
  const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
1128
1402
 
1129
1403
  return {
@@ -1131,9 +1405,9 @@ const forbiddenPatterns = [
1131
1405
  reason: `Hallucinated "${capability}" capability in ${lang}...`,
1132
1406
  detected: match ? match[0].trim() : 'unknown pattern',
1133
1407
  language: lang,
1134
- african_language_detected: isAfrican,
1408
+ african_language_detected: isAfrican,
1135
1409
  financial_expression_found: isFinancial,
1136
- capability_attempted: capability
1410
+ capability_attempted: capability
1137
1411
  };
1138
1412
  }
1139
1413
  }
@@ -1144,22 +1418,22 @@ const forbiddenPatterns = [
1144
1418
  // -----------------------------
1145
1419
  // ✅ CRITICAL FIX: Resolver output unwrapping helper
1146
1420
  // -----------------------------
1147
- _unwrapResolverResult(result) {
1148
- if (result && typeof result === 'object') {
1149
- if (result.output !== undefined) return result.output;
1150
- if (result.response !== undefined) return result.response; // ✅ ADD THIS
1151
- if (result.text !== undefined) return result.text;
1152
- if (result.content !== undefined) return result.content;
1421
+ _unwrapResolverResult(result) {
1422
+ if (result && typeof result === 'object') {
1423
+ if (result.output !== undefined) return result.output;
1424
+ if (result.response !== undefined) return result.response; // ✅ ADD THIS
1425
+ if (result.text !== undefined) return result.text;
1426
+ if (result.content !== undefined) return result.content;
1427
+ }
1428
+ return result;
1153
1429
  }
1154
- return result;
1155
- }
1156
1430
 
1157
1431
  // -----------------------------
1158
1432
  // Step execution (WHERE RESOLVERS ARE INVOKED)
1159
1433
  // -----------------------------
1160
1434
  async executeStep(step, agentResolver) {
1161
1435
  const stepType = step.type;
1162
-
1436
+
1163
1437
  // ✅ Enforce per-step constraints (basic validation)
1164
1438
  if (step.constraints && Object.keys(step.constraints).length > 0) {
1165
1439
  for (const [key, value] of Object.entries(step.constraints)) {
@@ -1235,7 +1509,7 @@ _unwrapResolverResult(result) {
1235
1509
  this.context[`__resolver_${idx}`] = result;
1236
1510
  return this._unwrapResolverResult(result);
1237
1511
  }
1238
-
1512
+
1239
1513
  resolverAttempts.push({
1240
1514
  name: resolverName,
1241
1515
  status: 'skipped',
@@ -1303,7 +1577,7 @@ _unwrapResolverResult(result) {
1303
1577
  errorMessage += ` → Verify API keys/tokens are set in environment variables\n`;
1304
1578
  }
1305
1579
  }
1306
-
1580
+
1307
1581
  errorMessage += `\n• Resolver documentation:\n`;
1308
1582
  let hasDocs = false;
1309
1583
  resolverAttempts.forEach(attempt => {
@@ -1319,7 +1593,7 @@ _unwrapResolverResult(result) {
1319
1593
  throw new Error(errorMessage);
1320
1594
  };
1321
1595
 
1322
- switch (stepType) {
1596
+ switch (stepType) {
1323
1597
  case 'calculate': {
1324
1598
  // ✅ Interpolate variables in the expression before evaluation
1325
1599
  let expr = step.expression || step.actionRaw;
@@ -1330,7 +1604,7 @@ _unwrapResolverResult(result) {
1330
1604
  if (step.saveAs) this.context[step.saveAs] = result;
1331
1605
  break;
1332
1606
  }
1333
-
1607
+
1334
1608
  case 'action': {
1335
1609
  let action = this._safeInterpolate(
1336
1610
  step.actionRaw,
@@ -1428,7 +1702,7 @@ _unwrapResolverResult(result) {
1428
1702
  }
1429
1703
  break;
1430
1704
  }
1431
-
1705
+
1432
1706
  case 'use': {
1433
1707
  const tool = this._safeInterpolate(step.tool, this.context, 'tool name');
1434
1708
  const rawResult = await runResolvers(`Use ${tool}`);
@@ -1436,7 +1710,7 @@ _unwrapResolverResult(result) {
1436
1710
  if (step.saveAs) this.context[step.saveAs] = unwrapped;
1437
1711
  break;
1438
1712
  }
1439
-
1713
+
1440
1714
  case 'ask': {
1441
1715
  const target = this._safeInterpolate(step.target, this.context, 'LLM prompt');
1442
1716
  if (/{[^}]+}/.test(target)) {
@@ -1461,7 +1735,7 @@ _unwrapResolverResult(result) {
1461
1735
  if (step.saveAs) this.context[step.saveAs] = unwrapped;
1462
1736
  break;
1463
1737
  }
1464
-
1738
+
1465
1739
  case 'evolve': {
1466
1740
  const { targetResolver, feedback } = step;
1467
1741
  if (this.verbose) {
@@ -1483,14 +1757,14 @@ _unwrapResolverResult(result) {
1483
1757
  }
1484
1758
  break;
1485
1759
  }
1486
-
1760
+
1487
1761
  case 'if': {
1488
1762
  if (this.evaluateCondition(step.condition, this.context)) {
1489
1763
  for (const s of step.body) await this.executeStep(s, agentResolver);
1490
1764
  }
1491
1765
  break;
1492
1766
  }
1493
-
1767
+
1494
1768
  case 'parallel': {
1495
1769
  const { steps, timeout } = step;
1496
1770
  if (timeout !== undefined && timeout > 0) {
@@ -1514,7 +1788,7 @@ _unwrapResolverResult(result) {
1514
1788
  }
1515
1789
  break;
1516
1790
  }
1517
-
1791
+
1518
1792
  case 'escalation': {
1519
1793
  const { levels } = step;
1520
1794
  let finalResult = null;
@@ -1566,17 +1840,17 @@ _unwrapResolverResult(result) {
1566
1840
  }
1567
1841
  break;
1568
1842
  }
1569
-
1843
+
1570
1844
  case 'connect': {
1571
1845
  this.resources[step.resource] = step.endpoint;
1572
1846
  break;
1573
1847
  }
1574
-
1848
+
1575
1849
  case 'agent_use': {
1576
1850
  this.agentMap[step.logicalName] = step.resource;
1577
1851
  break;
1578
1852
  }
1579
-
1853
+
1580
1854
  case 'debrief': {
1581
1855
  if (step.message.includes('{')) {
1582
1856
  const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
@@ -1588,14 +1862,14 @@ _unwrapResolverResult(result) {
1588
1862
  this.emit('debrief', { agent: step.agent, message: step.message });
1589
1863
  break;
1590
1864
  }
1591
-
1865
+
1592
1866
  case 'prompt': {
1593
1867
  if (this.verbose) {
1594
1868
  console.log(`❓ Prompt: ${step.question}`);
1595
1869
  }
1596
1870
  break;
1597
1871
  }
1598
-
1872
+
1599
1873
  case 'emit': {
1600
1874
  const payloadTemplate = step.payload;
1601
1875
  const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
@@ -1623,7 +1897,7 @@ _unwrapResolverResult(result) {
1623
1897
  }
1624
1898
  break;
1625
1899
  }
1626
-
1900
+
1627
1901
  case 'persist': {
1628
1902
  if (!this._requireSemantic(step.variable, 'persist')) {
1629
1903
  if (this.verbose) {
@@ -1649,7 +1923,7 @@ _unwrapResolverResult(result) {
1649
1923
  }
1650
1924
  break;
1651
1925
  }
1652
-
1926
+
1653
1927
  case 'persist-db': {
1654
1928
  if (!this.dbClient) {
1655
1929
  this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
@@ -1682,7 +1956,7 @@ _unwrapResolverResult(result) {
1682
1956
  const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
1683
1957
  await db.collection(step.collection).insertOne({
1684
1958
  workflow_name: this.context.workflow_name || 'unknown',
1685
- sourceValue,
1959
+ sourceValue,
1686
1960
  created_at: new Date()
1687
1961
  });
1688
1962
  break;
@@ -1717,18 +1991,20 @@ _unwrapResolverResult(result) {
1717
1991
  if (workflow.type !== 'workflow') {
1718
1992
  throw new Error(`Unknown workflow type: ${workflow.type}`);
1719
1993
  }
1720
-
1994
+
1721
1995
  this.context = {
1722
1996
  ...inputs,
1723
1997
  workflow_name: workflow.name
1724
1998
  };
1725
1999
 
1726
- // ✅ NEW: Validate Inputs BEFORE any step runs
1727
- this._validateInputs(inputs);
2000
+ // ✅ NEW: Validate Inputs BEFORE any step runs
2001
+ // In redact mode: PII tokens are replaced in place, execution continues.
2002
+ // In block mode (default): throws on any PII or financial intent match.
2003
+ this._validateInputs(inputs);
1728
2004
 
1729
2005
  // ✅ AUDIT LOG: Workflow start (ENHANCED with governance metadata)
1730
2006
  const governanceHash = this._generateGovernanceProfileHash(workflow);
1731
-
2007
+
1732
2008
  this._createAuditEntry('workflow_started', {
1733
2009
  workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`, // ✅ Workflow ID
1734
2010
  workflow_name: workflow.name,
@@ -1739,6 +2015,7 @@ _unwrapResolverResult(result) {
1739
2015
  inputs_count: Object.keys(inputs).length,
1740
2016
  steps_count: workflow.steps.length,
1741
2017
  allowed_resolvers: workflow.allowedResolvers || [],
2018
+ pii_mode: this.piiMode, // ✅ NEW v1.3.0 — surfaced in audit
1742
2019
  constraints: {
1743
2020
  max_generations: workflow.maxGenerations,
1744
2021
  strict_inputs: process.env.OLANG_STRICT_INPUTS === 'true'
@@ -1773,7 +2050,7 @@ _unwrapResolverResult(result) {
1773
2050
  for (const step of workflow.steps) {
1774
2051
  await this.executeStep(step, agentResolver);
1775
2052
  }
1776
-
2053
+
1777
2054
  // ✅ AUDIT LOG: Workflow completion (ENHANCED)
1778
2055
  this._createAuditEntry('workflow_completed', {
1779
2056
  workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`,