@o-lang/olang 1.2.38 → 1.4.0-alpha.1

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.4.0-alpha.1'; // 🔁 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
+ ];
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' },
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);
38
391
  }
39
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,38 +997,131 @@ 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
+ /**
1001
+ * evaluateCondition(cond, ctx)
1002
+ *
1003
+ * Governance Features:
1004
+ * 1. Quote-Aware Parsing: Prevents splitting on "or"/"and" inside quoted strings.
1005
+ * 2. Strict Equality: Uses === to prevent type-coercion safety bypasses.
1006
+ * 3. Comprehensive Operators: Supports gte, lte, contains, not equals.
1007
+ * 4. Auditability: Warns on unrecognised syntax to prevent silent failures.
1008
+ */
1009
+ evaluateCondition(cond, ctx) {
1010
+ if (!cond) return false;
642
1011
  cond = cond.trim();
643
1012
 
644
- // 1. Handle Logical OR (|| or 'or')
645
- if (/\|\||\bor\b/i.test(cond)) {
646
- return cond.split(/\|\||\bor\b/i).some(c => this.evaluateCondition(c.trim(), ctx));
1013
+ // ── Helper: split on logical operators OUTSIDE quoted strings ────────────
1014
+ const splitOutsideQuotes = (str, regex) => {
1015
+ const parts = [];
1016
+ let current = '';
1017
+ let inQuote = false;
1018
+ let quoteChar = '';
1019
+
1020
+ for (let i = 0; i < str.length; i++) {
1021
+ const ch = str[i];
1022
+
1023
+ // Handle quote toggling
1024
+ if (!inQuote && (ch === '"' || ch === "'")) {
1025
+ inQuote = true;
1026
+ quoteChar = ch;
1027
+ current += ch;
1028
+ } else if (inQuote && ch === quoteChar) {
1029
+ // Check for escaped quote? For now, simple toggle.
1030
+ inQuote = false;
1031
+ quoteChar = '';
1032
+ current += ch;
1033
+ } else if (!inQuote) {
1034
+ // Check for operator match at current position
1035
+ const remaining = str.slice(i);
1036
+ const m = remaining.match(regex);
1037
+ if (m && m.index === 0) {
1038
+ parts.push(current);
1039
+ current = '';
1040
+ i += m[0].length - 1;
1041
+ continue;
1042
+ } else {
1043
+ current += ch;
1044
+ }
1045
+ } else {
1046
+ current += ch;
1047
+ }
1048
+ }
1049
+ parts.push(current);
1050
+ return parts.map(p => p.trim()).filter(Boolean);
1051
+ };
1052
+
1053
+ // ── 1. Logical OR ─────────────────────────────────────────────────────────
1054
+ // (?!\s+equal) prevents splitting on "or" in "greater than or equal"
1055
+ const orParts = splitOutsideQuotes(cond, /^(\|\||\bor\b(?!\s+equal))/i);
1056
+ if (orParts.length > 1) {
1057
+ return orParts.some(c => this.evaluateCondition(c.trim(), ctx));
647
1058
  }
648
- // ✅ 2. Handle Logical AND (&& or 'and')
649
- if (/&&|\band\b/i.test(cond)) {
650
- return cond.split(/&&|\band\b/i).every(c => this.evaluateCondition(c.trim(), ctx));
1059
+
1060
+ // ── 2. Logical AND ────────────────────────────────────────────────────────
1061
+ const andParts = splitOutsideQuotes(cond, /^(&&|\band\b)/i);
1062
+ if (andParts.length > 1) {
1063
+ return andParts.every(c => this.evaluateCondition(c.trim(), ctx));
651
1064
  }
652
1065
 
653
- // 3. Handle == or === (works with or without {})
654
- const eqMatch = cond.match(/^(?:\{(.+)\}|(\w+))\s*===?\s*"(.*)"$/);
1066
+ // ── 3. Strict equality: {var} === "value" or {var} == "value" ────────────
1067
+ const eqMatch = cond.match(/^(?:\{(.+?)\}|(\w[\w.]*?))\s*===?\s*"(.*)"$/);
655
1068
  if (eqMatch) {
656
1069
  const key = eqMatch[1] || eqMatch[2];
657
1070
  return this.getNested(ctx, key) === eqMatch[3];
658
1071
  }
659
1072
 
660
- // 4. Keep original O-Lang syntax
661
- const oldEq = cond.match(/^\{(.+)\}\s+equals\s+"(.*)"$/);
662
- if (oldEq) return this.getNested(ctx, oldEq[1]) == oldEq[2];
1073
+ // ── 4. Not equals: {var} != "value" or {var} !== "value" ─────────────────
1074
+ const neqMatch = cond.match(/^(?:\{(.+?)\}|(\w[\w.]*?))\s*!==?\s*"(.*)"$/);
1075
+ if (neqMatch) {
1076
+ const key = neqMatch[1] || neqMatch[2];
1077
+ return this.getNested(ctx, key) !== neqMatch[3];
1078
+ }
1079
+
1080
+ // ── 5. O-Lang keyword: {var} equals "value" (strict) ─────────────────────
1081
+ const oldEq = cond.match(/^\{(.+?)\}\s+equals\s+"(.*)"$/);
1082
+ if (oldEq) return this.getNested(ctx, oldEq[1]) === oldEq[2];
1083
+
1084
+ // ── 6. O-Lang keyword: {var} not equals "value" ───────────────────────────
1085
+ const notEq = cond.match(/^\{(.+?)\}\s+not\s+equals\s+"(.*)"$/);
1086
+ if (notEq) return this.getNested(ctx, notEq[1]) !== notEq[2];
1087
+
1088
+ // ── 7. Contains: {var} contains "value" ──────────────────────────────────
1089
+ const containsMatch = cond.match(/^\{(.+?)\}\s+contains\s+"(.*)"$/);
1090
+ if (containsMatch) {
1091
+ const value = this.getNested(ctx, containsMatch[1]);
1092
+ const target = containsMatch[2];
1093
+ if (Array.isArray(value)) return value.includes(target);
1094
+ if (typeof value === 'string') return value.includes(target);
1095
+ return false;
1096
+ }
1097
+
1098
+ // ── 8. Numeric comparisons (GTE/LTE before GT/LT) ────────────────────────
1099
+ const gte = cond.match(/^\{(.+?)\}\s+greater than or equal\s+(\d+\.?\d*)$/);
1100
+ if (gte) return parseFloat(this.getNested(ctx, gte[1])) >= parseFloat(gte[2]);
663
1101
 
664
- const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
1102
+ const lte = cond.match(/^\{(.+?)\}\s+less than or equal\s+(\d+\.?\d*)$/);
1103
+ if (lte) return parseFloat(this.getNested(ctx, lte[1])) <= parseFloat(lte[2]);
1104
+
1105
+ const gt = cond.match(/^\{(.+?)\}\s+greater than\s+(\d+\.?\d*)$/);
665
1106
  if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
666
1107
 
667
- const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
1108
+ const lt = cond.match(/^\{(.+?)\}\s+less than\s+(\d+\.?\d*)$/);
668
1109
  if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
669
1110
 
670
- // Fallback: truthy check
671
- return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
1111
+ // ── 9. Truthy fallback — warn so authors know it fired ───────────────────
1112
+ const fallbackKey = cond.replace(/^\{|\}$/g, '');
1113
+ const fallbackValue = this.getNested(ctx, fallbackKey);
1114
+
1115
+ this.addWarning(
1116
+ `evaluateCondition: unrecognised condition syntax "${cond}" — ` +
1117
+ `falling back to truthy check on "${fallbackKey}" ` +
1118
+ `(value: ${JSON.stringify(fallbackValue)}). ` +
1119
+ `If this is unintentional, check your condition syntax.`
1120
+ );
1121
+
1122
+ return Boolean(fallbackValue);
672
1123
  }
1124
+
673
1125
  mathFunctions = {
674
1126
  add: (a, b) => a + b,
675
1127
  subtract: (a, b) => a - b,
@@ -690,38 +1142,38 @@ class RuntimeAPI {
690
1142
  abs: a => Math.abs(a)
691
1143
  };
692
1144
 
693
- evaluateMath(expr) {
1145
+ evaluateMath(expr) {
694
1146
  // ✅ Handle quoted string literals with interpolation: "{var}" → interpolated string
695
1147
  if (typeof expr === 'string') {
696
1148
  const trimmed = expr.trim();
697
-
1149
+
698
1150
  // Check if it's a quoted string (single or double quotes)
699
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
1151
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
700
1152
  (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
701
1153
  // Extract the inner content
702
1154
  let inner = trimmed.slice(1, -1);
703
-
1155
+
704
1156
  // Perform interpolation: replace {var} with context values
705
1157
  inner = inner.replace(/\{([^\}]+)\}/g, (_, path) => {
706
1158
  const value = this.getNested(this.context, path.trim());
707
1159
  return value !== undefined ? String(value) : `{${path}}`;
708
1160
  });
709
-
1161
+
710
1162
  return inner;
711
1163
  }
712
1164
  }
713
-
1165
+
714
1166
  // ── Original math evaluation logic (unchanged) ──────────────────────────
715
1167
  expr = expr.replace(/\{([^\}]+)\}/g, (_, path) => {
716
1168
  const value = this.getNested(this.context, path.trim());
717
1169
  if (typeof value === 'string') return `"${value.replace(/"/g, '\\"')}"`;
718
1170
  return value !== undefined ? value : 0;
719
1171
  });
720
-
1172
+
721
1173
  const funcNames = Object.keys(this.mathFunctions);
722
1174
  const safeFunc = {};
723
1175
  funcNames.forEach(fn => safeFunc[fn] = this.mathFunctions[fn]);
724
-
1176
+
725
1177
  try {
726
1178
  const f = new Function(...funcNames, `return ${expr};`);
727
1179
  return f(...funcNames.map(fn => safeFunc[fn]));
@@ -765,350 +1217,264 @@ class RuntimeAPI {
765
1217
  });
766
1218
  }
767
1219
 
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' },
1220
+ // ================================
1221
+ // ✅ UPDATED v1.3.0 — KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety)
1222
+ //
1223
+ // BACKWARD COMPAT:
1224
+ // OLANG_PII_MODE=block (default) original behaviour, throws on any match
1225
+ // OLANG_PII_MODE=redact → replaces PII tokens, continues
1226
+ // OLANG_PII_MODE=redact-and-log → redact + audit entry per redaction event
1227
+ //
1228
+ // Financial intent patterns always throw regardless of PII mode.
1229
+ // ================================
1230
+ _validateInputs(inputs) {
1231
+ // Only scan specific input fields that contain user text
1232
+ // NOTE: 'document_text' intentionally excluded — legal documents legitimately
1233
+ // contain financial terms and must not be blocked at the input layer.
1234
+ const fieldsToScan = ['user_message', 'user_question', 'text', 'prompt'];
1235
+
1236
+ const redactMode = this.piiMode === 'redact' || this.piiMode === 'redact-and-log';
1237
+ const allRedactions = {};
1238
+
1239
+ // ── PASS 1: PII redaction (when mode allows) ──────────────────────────
1240
+ if (redactMode) {
1241
+ for (const field of fieldsToScan) {
1242
+ const text = inputs[field];
1243
+ if (!text || typeof text !== 'string') continue;
1244
+
1245
+ const { redacted, redactions, wasModified } = this._redactPII(text);
1246
+
1247
+ if (wasModified) {
1248
+ inputs[field] = redacted; // mutate in place — caller sees clean value
1249
+ allRedactions[field] = redactions;
1250
+
1251
+ if (this.piiMode === 'redact-and-log') {
1252
+ this._createAuditEntry('pii_redacted', {
1253
+ field,
1254
+ redaction_count: redactions.length,
1255
+ redactions: redactions.map(r => ({
1256
+ label: r.replacement,
1257
+ capability: r.capability,
1258
+ lang: r.lang
1259
+ // NOTE: original value intentionally excluded from audit log
1260
+ // to avoid persisting the very PII we just redacted
1261
+ })),
1262
+ severity: 'info'
1263
+ });
1264
+ }
811
1265
 
1266
+ if (this.verbose) {
1267
+ console.log(
1268
+ `🛡️ [O-Lang PII] Redacted ${redactions.length} item(s) in "${field}": ` +
1269
+ redactions.map(r => r.replacement).join(', ')
1270
+ );
1271
+ }
1272
+ }
1273
+ }
1274
+ }
812
1275
 
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' },
1276
+ // ── PASS 2: Financial intent scan (always blocks, mode-independent) ───
1277
+ for (const field of fieldsToScan) {
1278
+ const text = inputs[field]; // may already be PII-redacted from pass 1
1279
+ if (!text || typeof text !== 'string') continue;
820
1280
 
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' },
1281
+ // 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION (INPUT)
1282
+ for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
1283
+ if (pattern.test(text)) {
1284
+ const match = text.match(pattern);
1285
+ const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
1286
+ const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
830
1287
 
1288
+ // ✅ DECOUPLED: Check legal context via standardized signals (not UI fields)
1289
+ const intent = this.context.__verified_intent || {};
1290
+ const signals = intent.context_signals || {};
1291
+
1292
+ const isLegalContext =
1293
+ // Signal 1: Explicit scope declaration
1294
+ intent.scope === 'legal_analysis_only' ||
1295
+
1296
+ // Signal 2: Standardized context signals (server-mapped, UI-agnostic)
1297
+ signals.isLegalDocument === true ||
1298
+ signals.documentCategory === 'contract' ||
1299
+ signals.documentCategory === 'nda' ||
1300
+ signals.documentCategory === 'agreement' ||
1301
+ signals.documentCategory === 'legal' ||
1302
+
1303
+ // Signal 3: Semantic fallback (works even if signals missing)
1304
+ (typeof text === 'string' && /clause|term|agreement|contract|obligation|penalty|damages|breach|party|shall|herein/i.test(text));
1305
+
1306
+ // ✅ NEW: Check contextual allowlist if in legal context
1307
+ if (isLegalContext && this.context.__verified_intent?.contextual_allowlist) {
1308
+ const allowlist = this.context.__verified_intent.contextual_allowlist;
1309
+ const triggerWord = match ? match[0].toLowerCase() : '';
1310
+
1311
+ const allowed = allowlist.some(rule => {
1312
+ // Check if this pattern's capability matches the rule's trigger
1313
+ const triggerMatch =
1314
+ triggerWord.includes(rule.trigger.toLowerCase()) ||
1315
+ capability.toLowerCase().includes(rule.trigger.toLowerCase()); // ← Handle capability-level triggers
1316
+
1317
+ if (triggerMatch) {
1318
+ // Check if required legal keywords are present
1319
+ return rule.requires.some(keyword =>
1320
+ text.toLowerCase().includes(keyword.toLowerCase())
1321
+ );
1322
+ }
1323
+ return false;
1324
+ });
831
1325
 
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' },
1326
+ if (allowed) {
1327
+ // ✅ AUDIT LOG: Contextual allowlist bypass
1328
+ this._createAuditEntry('safety_bypass', {
1329
+ type: 'contextual_allowlist',
1330
+ trigger: triggerWord,
1331
+ legal_context: true,
1332
+ matched_keywords: this.context.__verified_intent.contextual_allowlist
1333
+ .find(r => triggerWord.includes(r.trigger.toLowerCase()))?.requires || [],
1334
+ severity: 'info'
1335
+ });
1336
+ continue; // Skip blocking this match
1337
+ }
1338
+ }
848
1339
 
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' },
1340
+ // ✅ AUDIT LOG: Input Safety Violation (only if not bypassed)
1341
+ this._createAuditEntry('input_safety_violation', {
1342
+ type: 'blocked_input',
1343
+ field: field,
1344
+ detected_phrase: match ? match[0].trim() : 'unknown pattern',
1345
+ capability: capability,
1346
+ language: lang,
1347
+ african_language_detected: isAfrican,
1348
+ financial_expression_found: isFinancial,
1349
+ legal_context_detected: false,
1350
+ severity: 'high'
1351
+ });
857
1352
 
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
- ];
1353
+ throw new Error(
1354
+ `[O-Lang SAFETY] Blocked Input in "${lang}":\n` +
1355
+ ` → Detected: "${match ? match[0].trim() : 'Pattern Match'}"\n` +
1356
+ ` → Capability: ${capability}\n` +
1357
+ ` → Field: ${field}\n` +
1358
+ ` → African Language Detected: ${isAfrican}\n` +
1359
+ ` → Financial Expression: ${isFinancial}\n` +
1360
+ `\n🛑 Workflow halted before execution.`
1361
+ );
1362
+ }
1363
+ }
1364
+ }
866
1365
 
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'
1366
+ // ── PASS 3: PII block scan (only in block mode original behaviour) ──
1367
+ if (!redactMode) {
1368
+ for (const field of fieldsToScan) {
1369
+ const text = inputs[field];
1370
+ if (!text || typeof text !== 'string') continue;
1371
+
1372
+ for (const { pattern, capability, lang, label } of this._getPIIPatterns()) {
1373
+ if (pattern.test(text)) {
1374
+ const match = text.match(pattern);
1375
+ const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
1376
+
1377
+ this._createAuditEntry('input_safety_violation', {
1378
+ type: 'blocked_input',
1379
+ field: field,
1380
+ detected_phrase: match ? match[0].trim() : 'unknown pattern',
1381
+ capability: capability,
1382
+ language: lang,
1383
+ african_language_detected: isAfrican,
1384
+ financial_expression_found: false,
1385
+ pii_type: label,
1386
+ severity: 'high'
921
1387
  });
922
- continue; // Skip blocking this match
1388
+
1389
+ throw new Error(
1390
+ `[O-Lang SAFETY] Blocked PII in field "${field}" (${lang}):\n` +
1391
+ ` → Type: ${label}\n` +
1392
+ ` → Capability: ${capability}\n` +
1393
+ `\n🛑 Workflow halted before execution. Set OLANG_PII_MODE=redact to auto-redact instead.`
1394
+ );
923
1395
  }
924
1396
  }
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
1397
  }
950
1398
  }
1399
+
1400
+ return {
1401
+ passed: true,
1402
+ redactions: Object.keys(allRedactions).length > 0 ? allRedactions : null
1403
+ };
951
1404
  }
952
- return { passed: true };
953
- }
954
1405
 
955
- // -----------------------------
956
- // ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION (CONJUGATION-AWARE + EVASION-RESISTANT)
957
- // -----------------------------
1406
+ // ================================
1407
+ // ✅ UPDATED v1.3.0 — KERNEL-LEVEL LLM HALLUCINATION PREVENTION
1408
+ // (CONJUGATION-AWARE + EVASION-RESISTANT)
1409
+ //
1410
+ // Now uses _getFinancialIntentPatterns() instead of inline duplicate.
1411
+ // Xhosa patterns included automatically via the shared set.
1412
+ // All __verified_intent logic unchanged for backward compat.
1413
+ // ================================
958
1414
  _validateLLMOutput(output, actionContext) {
959
1415
  if (!output || typeof output !== 'string') return { passed: true };
960
1416
 
961
1417
  // ── __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
- };
1418
+ const intent = this.context.__verified_intent;
1419
+ if (intent) {
1420
+ if (intent.prohibited_actions && Array.isArray(intent.prohibited_actions)) {
1421
+ const lower = output.toLowerCase();
1422
+ for (const action of intent.prohibited_actions) {
1423
+ if (lower.includes(action.toLowerCase())) {
1424
+ return {
1425
+ passed: false,
1426
+ reason: `Output violates prohibited action "${action}" defined in __verified_intent`,
1427
+ detected: action,
1428
+ language: 'multi'
1429
+ };
1430
+ }
1431
+ }
974
1432
  }
975
- }
976
- }
977
1433
 
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 = '';
1434
+ if (intent.prohibited_topics && Array.isArray(intent.prohibited_topics)) {
1435
+ for (const topic of intent.prohibited_topics) {
1436
+ const isRegex = typeof topic === 'object' && topic.pattern;
1437
+ let matched = false;
1438
+ let detected = '';
1439
+
1440
+ if (isRegex) {
1441
+ try {
1442
+ const re = new RegExp(topic.pattern, topic.flags || 'i');
1443
+ const match = output.match(re);
1444
+ matched = !!match;
1445
+ detected = match ? match[0] : topic.pattern;
1446
+ } catch (e) {
1447
+ this.addWarning(`Invalid prohibited_topic regex: "${topic.pattern}" — ${e.message}`);
1448
+ continue;
1449
+ }
1450
+ } else {
1451
+ matched = output.toLowerCase().includes(topic.toLowerCase());
1452
+ detected = topic;
1453
+ }
983
1454
 
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;
1455
+ if (matched) {
1456
+ return {
1457
+ passed: false,
1458
+ reason: `Output violates prohibited topic "${isRegex ? topic.pattern : topic}" defined in __verified_intent`,
1459
+ detected,
1460
+ language: 'multi'
1461
+ };
1462
+ }
993
1463
  }
994
- } else {
995
- matched = output.toLowerCase().includes(topic.toLowerCase());
996
- detected = topic;
997
1464
  }
998
1465
 
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
- }
1466
+ // __verified_intent present and passed — skip hardcoded patterns
1467
+ return { passed: true };
1007
1468
  }
1008
- }
1009
1469
 
1010
- // __verified_intent present and passed skip hardcoded patterns
1011
- return { passed: true };
1012
- }
1013
-
1014
- // ── No __verified_intent — fall through to hardcoded patterns ─────────────
1470
+ // ── No __verified_intent fall through to shared pattern set ─────────────
1015
1471
  // 🔑 Extract allowed capabilities from workflow allowlist
1016
1472
  const allowedCapabilities = Array.from(this.allowedResolvers)
1017
1473
  .filter(name => !name.startsWith('llm-') && name !== 'builtInMathResolver')
1018
1474
  .map(name => name.replace('@o-lang/', '').replace(/-resolver$/, ''));
1019
1475
 
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) {
1476
+ // 🔒 SCAN OUTPUT FOR FORBIDDEN INTENTS (shared set — includes Xhosa)
1477
+ for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
1112
1478
  if (pattern.test(output)) {
1113
1479
  const hasCapability = allowedCapabilities.some(c =>
1114
1480
  c.includes(capability) ||
@@ -1121,9 +1487,9 @@ const forbiddenPatterns = [
1121
1487
 
1122
1488
  if (!hasCapability) {
1123
1489
  const match = output.match(pattern);
1124
-
1490
+
1125
1491
  // ✅ Explicitly flag African & Financial context for Audit Logs
1126
- const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'am', 'om', 'ff', 'so', 'sn'].includes(lang);
1492
+ const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
1127
1493
  const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
1128
1494
 
1129
1495
  return {
@@ -1131,9 +1497,9 @@ const forbiddenPatterns = [
1131
1497
  reason: `Hallucinated "${capability}" capability in ${lang}...`,
1132
1498
  detected: match ? match[0].trim() : 'unknown pattern',
1133
1499
  language: lang,
1134
- african_language_detected: isAfrican,
1500
+ african_language_detected: isAfrican,
1135
1501
  financial_expression_found: isFinancial,
1136
- capability_attempted: capability
1502
+ capability_attempted: capability
1137
1503
  };
1138
1504
  }
1139
1505
  }
@@ -1144,22 +1510,22 @@ const forbiddenPatterns = [
1144
1510
  // -----------------------------
1145
1511
  // ✅ CRITICAL FIX: Resolver output unwrapping helper
1146
1512
  // -----------------------------
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;
1513
+ _unwrapResolverResult(result) {
1514
+ if (result && typeof result === 'object') {
1515
+ if (result.output !== undefined) return result.output;
1516
+ if (result.response !== undefined) return result.response; // ✅ ADD THIS
1517
+ if (result.text !== undefined) return result.text;
1518
+ if (result.content !== undefined) return result.content;
1519
+ }
1520
+ return result;
1153
1521
  }
1154
- return result;
1155
- }
1156
1522
 
1157
1523
  // -----------------------------
1158
1524
  // Step execution (WHERE RESOLVERS ARE INVOKED)
1159
1525
  // -----------------------------
1160
1526
  async executeStep(step, agentResolver) {
1161
1527
  const stepType = step.type;
1162
-
1528
+
1163
1529
  // ✅ Enforce per-step constraints (basic validation)
1164
1530
  if (step.constraints && Object.keys(step.constraints).length > 0) {
1165
1531
  for (const [key, value] of Object.entries(step.constraints)) {
@@ -1235,7 +1601,7 @@ _unwrapResolverResult(result) {
1235
1601
  this.context[`__resolver_${idx}`] = result;
1236
1602
  return this._unwrapResolverResult(result);
1237
1603
  }
1238
-
1604
+
1239
1605
  resolverAttempts.push({
1240
1606
  name: resolverName,
1241
1607
  status: 'skipped',
@@ -1303,7 +1669,7 @@ _unwrapResolverResult(result) {
1303
1669
  errorMessage += ` → Verify API keys/tokens are set in environment variables\n`;
1304
1670
  }
1305
1671
  }
1306
-
1672
+
1307
1673
  errorMessage += `\n• Resolver documentation:\n`;
1308
1674
  let hasDocs = false;
1309
1675
  resolverAttempts.forEach(attempt => {
@@ -1319,7 +1685,7 @@ _unwrapResolverResult(result) {
1319
1685
  throw new Error(errorMessage);
1320
1686
  };
1321
1687
 
1322
- switch (stepType) {
1688
+ switch (stepType) {
1323
1689
  case 'calculate': {
1324
1690
  // ✅ Interpolate variables in the expression before evaluation
1325
1691
  let expr = step.expression || step.actionRaw;
@@ -1330,7 +1696,7 @@ _unwrapResolverResult(result) {
1330
1696
  if (step.saveAs) this.context[step.saveAs] = result;
1331
1697
  break;
1332
1698
  }
1333
-
1699
+
1334
1700
  case 'action': {
1335
1701
  let action = this._safeInterpolate(
1336
1702
  step.actionRaw,
@@ -1428,7 +1794,7 @@ _unwrapResolverResult(result) {
1428
1794
  }
1429
1795
  break;
1430
1796
  }
1431
-
1797
+
1432
1798
  case 'use': {
1433
1799
  const tool = this._safeInterpolate(step.tool, this.context, 'tool name');
1434
1800
  const rawResult = await runResolvers(`Use ${tool}`);
@@ -1436,7 +1802,7 @@ _unwrapResolverResult(result) {
1436
1802
  if (step.saveAs) this.context[step.saveAs] = unwrapped;
1437
1803
  break;
1438
1804
  }
1439
-
1805
+
1440
1806
  case 'ask': {
1441
1807
  const target = this._safeInterpolate(step.target, this.context, 'LLM prompt');
1442
1808
  if (/{[^}]+}/.test(target)) {
@@ -1461,7 +1827,7 @@ _unwrapResolverResult(result) {
1461
1827
  if (step.saveAs) this.context[step.saveAs] = unwrapped;
1462
1828
  break;
1463
1829
  }
1464
-
1830
+
1465
1831
  case 'evolve': {
1466
1832
  const { targetResolver, feedback } = step;
1467
1833
  if (this.verbose) {
@@ -1483,173 +1849,655 @@ _unwrapResolverResult(result) {
1483
1849
  }
1484
1850
  break;
1485
1851
  }
1486
-
1852
+
1853
+ // ─────────────────────────────────────────────────────────────────────────────
1854
+ // IF / ELSE-IF / ELSE
1855
+ //
1856
+ // Governance Features:
1857
+ // 1. Exclusive Branching: Only one branch executes (if -> else-if -> else).
1858
+ // 2. Semantic Validation: Checks symbols in conditions before evaluation.
1859
+ // 3. Auditability: Logs which condition was evaluated and which branch fired.
1860
+ // ─────────────────────────────────────────────────────────────────────────────
1861
+
1487
1862
  case 'if': {
1488
- if (this.evaluateCondition(step.condition, this.context)) {
1489
- for (const s of step.body) await this.executeStep(s, agentResolver);
1863
+ // 1. Validate all symbols referenced in the main condition
1864
+ const condSymbols = step.condition ? step.condition.match(/\{([^\}]+)\}/g) || [] : [];
1865
+ let symbolsValid = true;
1866
+
1867
+ for (const sym of condSymbols) {
1868
+ const key = sym.replace(/[{}]/g, '');
1869
+ if (!this._requireSemantic(key, 'if condition')) {
1870
+ symbolsValid = false;
1871
+ }
1872
+ }
1873
+
1874
+ if (!symbolsValid) {
1875
+ this._createAuditEntry('condition_skipped', {
1876
+ condition: step.condition,
1877
+ reason: 'One or more symbols missing in context',
1878
+ severity: 'warn'
1879
+ });
1880
+ break;
1881
+ }
1882
+
1883
+ // 2. Evaluate main if condition
1884
+ const mainPassed = this.evaluateCondition(step.condition, this.context);
1885
+
1886
+ this._createAuditEntry('condition_evaluated', {
1887
+ condition: step.condition,
1888
+ passed: mainPassed,
1889
+ branch: 'if',
1890
+ severity: 'info'
1891
+ });
1892
+
1893
+ if (mainPassed) {
1894
+ if (step.body && Array.isArray(step.body)) {
1895
+ for (const s of step.body) await this.executeStep(s, agentResolver);
1896
+ }
1897
+ break; // Exit after successful if
1898
+ }
1899
+
1900
+ // 3. else-if chain — stop at first match
1901
+ if (step.elseIf && Array.isArray(step.elseIf)) {
1902
+ let elseIfFired = false;
1903
+ for (const branch of step.elseIf) {
1904
+ // Validate symbols for else-if branch
1905
+ const branchSymbols = branch.condition ? branch.condition.match(/\{([^\}]+)\}/g) || [] : [];
1906
+ let branchSymbolsValid = true;
1907
+ for (const sym of branchSymbols) {
1908
+ const key = sym.replace(/[{}]/g, '');
1909
+ if (!this._requireSemantic(key, 'else-if condition')) {
1910
+ branchSymbolsValid = false;
1911
+ }
1912
+ }
1913
+
1914
+ if (!branchSymbolsValid) {
1915
+ this._createAuditEntry('condition_skipped', {
1916
+ condition: branch.condition,
1917
+ reason: 'One or more symbols missing in context',
1918
+ severity: 'warn'
1919
+ });
1920
+ continue; // Skip this else-if, try next
1921
+ }
1922
+
1923
+ const branchPassed = this.evaluateCondition(branch.condition, this.context);
1924
+
1925
+ this._createAuditEntry('condition_evaluated', {
1926
+ condition: branch.condition,
1927
+ passed: branchPassed,
1928
+ branch: 'else-if',
1929
+ severity: 'info'
1930
+ });
1931
+
1932
+ if (branchPassed) {
1933
+ if (branch.body && Array.isArray(branch.body)) {
1934
+ for (const s of branch.body) await this.executeStep(s, agentResolver);
1935
+ }
1936
+ elseIfFired = true;
1937
+ break;
1938
+ }
1939
+ }
1940
+ if (elseIfFired) break;
1941
+ }
1942
+
1943
+ // 4. else fallback
1944
+ if (step.elseBranch && Array.isArray(step.elseBranch)) {
1945
+ this._createAuditEntry('condition_evaluated', {
1946
+ condition: 'else',
1947
+ passed: true,
1948
+ branch: 'else',
1949
+ severity: 'info'
1950
+ });
1951
+ for (const s of step.elseBranch) await this.executeStep(s, agentResolver);
1490
1952
  }
1953
+
1491
1954
  break;
1492
1955
  }
1493
-
1956
+
1957
+ // ─────────────────────────────────────────────────────────────────────────────
1958
+ // PARALLEL
1959
+ //
1960
+ // Bugs fixed:
1961
+ // 1. Shared this.context race condition — all branches wrote to the same
1962
+ // object concurrently. Branch 2 restoring the snapshot overwrote whatever
1963
+ // branch 1 had just saved, so result_a was lost by the time it was read.
1964
+ // Fix: each branch gets its own context via Object.create(this), which
1965
+ // prototype-links to the parent (sharing allowedResolvers, auditLog,
1966
+ // events, verbose) but has its own context property that shadows the
1967
+ // parent's. Branches never touch this.context directly.
1968
+ //
1969
+ // 2. Promise.all → silent failure swallowing. A single rejection cancelled
1970
+ // all siblings. Fix: buildStepPromise catches internally and returns a
1971
+ // structured outcome, so Promise.all always resolves with the full set.
1972
+ //
1973
+ // 3. timed_out not always written. Fix: always written after settlement —
1974
+ // true on timeout, false on clean completion.
1975
+ //
1976
+ // 4. Losing Promise.race branch kept mutating this.context after the
1977
+ // workflow moved on. Fix: branches write to branchRuntime.context only;
1978
+ // this.context is only touched during the final merge step.
1979
+ // ─────────────────────────────────────────────────────────────────────────────
1980
+
1494
1981
  case 'parallel': {
1495
1982
  const { steps, timeout } = step;
1983
+
1984
+ if (!steps || !Array.isArray(steps) || steps.length === 0) {
1985
+ this.addWarning('Parallel step contains no sub-steps. Skipping.');
1986
+ break;
1987
+ }
1988
+
1989
+ // Snapshot context before any branch runs.
1990
+ // Every branch reads from this — not from each other.
1991
+ const contextSnapshot = { ...this.context };
1992
+
1993
+ // Build one promise per sub-step.
1994
+ //
1995
+ // Each branch runs against a prototype-linked clone of the runtime.
1996
+ // Object.create(this) shares: allowedResolvers, auditLog, events,
1997
+ // verbose, resources, agentMap — everything a step needs to execute.
1998
+ // But branchRuntime.context is its own property that shadows this.context,
1999
+ // so concurrent writes never collide.
2000
+ //
2001
+ // Errors are caught here and returned as structured outcomes so that
2002
+ // Promise.all always resolves with the full result set — one failure
2003
+ // does not cancel sibling branches.
2004
+ const buildStepPromise = async (s, index) => {
2005
+ const branchRuntime = Object.create(this);
2006
+ branchRuntime.context = { ...contextSnapshot };
2007
+
2008
+ try {
2009
+ await branchRuntime.executeStep(s, agentResolver);
2010
+
2011
+ const saveKey = s.saveAs || null;
2012
+ const value = saveKey ? branchRuntime.context[saveKey] : undefined;
2013
+
2014
+ return {
2015
+ status: 'fulfilled',
2016
+ index,
2017
+ saveAs: saveKey,
2018
+ value,
2019
+ stepType: s.type
2020
+ };
2021
+ } catch (error) {
2022
+ return {
2023
+ status: 'rejected',
2024
+ index,
2025
+ reason: error.message || String(error),
2026
+ stepType: s.type
2027
+ };
2028
+ }
2029
+ };
2030
+
2031
+ const runAllSteps = () =>
2032
+ Promise.all(steps.map((s, i) => buildStepPromise(s, i)));
2033
+
2034
+ let settledResults;
2035
+
1496
2036
  if (timeout !== undefined && timeout > 0) {
1497
- const timeoutPromise = new Promise(resolve => {
1498
- setTimeout(() => resolve({ timedOut: true }), timeout);
1499
- });
1500
- const parallelPromise = Promise.all(
1501
- steps.map(s => this.executeStep(s, agentResolver))
1502
- ).then(() => ({ timedOut: false }));
1503
- const result = await Promise.race([timeoutPromise, parallelPromise]);
1504
- this.context.timed_out = result.timedOut;
1505
- if (result.timedOut) {
1506
- this.emit('parallel_timeout', { duration: timeout, steps: steps.length });
2037
+ // Race: all steps vs timeout sentinel.
2038
+ // runAllSteps never rejects (errors caught inside buildStepPromise),
2039
+ // so Promise.race resolves with either the results array or null.
2040
+ const timeoutPromise = new Promise(resolve =>
2041
+ setTimeout(() => resolve(null), timeout)
2042
+ );
2043
+
2044
+ settledResults = await Promise.race([runAllSteps(), timeoutPromise]);
2045
+
2046
+ if (settledResults === null) {
2047
+ // Timeout won. Restore snapshot + mark timed_out.
2048
+ // Do NOT merge partial results — we cannot know which branches
2049
+ // completed cleanly before the cutoff.
2050
+ this.context = { ...contextSnapshot, timed_out: true };
2051
+
2052
+ this.emit('parallel_timeout', {
2053
+ duration: timeout,
2054
+ steps_count: steps.length
2055
+ });
2056
+
1507
2057
  if (this.verbose) {
1508
2058
  console.log(`⏰ Parallel execution timed out after ${timeout}ms`);
1509
2059
  }
2060
+
2061
+ this._createAuditEntry('parallel_timeout', {
2062
+ timeout_ms: timeout,
2063
+ steps_count: steps.length,
2064
+ severity: 'warn'
2065
+ });
2066
+
2067
+ break;
1510
2068
  }
2069
+
1511
2070
  } else {
1512
- await Promise.all(steps.map(s => this.executeStep(s, agentResolver)));
1513
- this.context.timed_out = false;
2071
+ settledResults = await runAllSteps();
2072
+ }
2073
+
2074
+ // Merge results back into this.context.
2075
+ // Start from the snapshot so pre-parallel state is the clean base,
2076
+ // then layer each branch's saveAs result on top.
2077
+ this.context = { ...contextSnapshot, timed_out: false };
2078
+
2079
+ for (const outcome of settledResults) {
2080
+ if (outcome.status === 'fulfilled') {
2081
+ const { saveAs, value } = outcome;
2082
+ if (saveAs !== null && value !== undefined) {
2083
+ this.context[saveAs] = value;
2084
+ }
2085
+ } else {
2086
+ const { reason, index } = outcome;
2087
+ this.addWarning(`Parallel step [index ${index}] failed: ${reason}`);
2088
+
2089
+ this.emit('parallel_step_failed', {
2090
+ index,
2091
+ reason,
2092
+ stepType: outcome.stepType
2093
+ });
2094
+
2095
+ this._createAuditEntry('parallel_step_failed', {
2096
+ step_index: index,
2097
+ reason,
2098
+ severity: 'high'
2099
+ });
2100
+ }
1514
2101
  }
2102
+
1515
2103
  break;
1516
2104
  }
1517
-
2105
+
2106
+ // ─────────────────────────────────────────────────────────────────────────────
2107
+ // ESCALATION
2108
+ //
2109
+ // Governance Features:
2110
+ // 1. Scope Safety: Fixes ReferenceError by declaring levelSteps in outer scope.
2111
+ // 2. Auditability: Logs level start, completion, timeout, and final outcome.
2112
+ // 3. Determinism: Ensures timed-out levels don't corrupt context.
2113
+ // ─────────────────────────────────────────────────────────────────────────────
2114
+
1518
2115
  case 'escalation': {
1519
2116
  const { levels } = step;
2117
+ const { parseBlock } = require('../parser');
2118
+
1520
2119
  let finalResult = null;
1521
- let currentTimeout = 0;
1522
2120
  let completedLevel = null;
2121
+
1523
2122
  for (const level of levels) {
2123
+ // Fix: Declare levelSteps in outer block scope to avoid ReferenceError
2124
+ // in the timed-out branch.
2125
+ const levelSteps = parseBlock(level.steps);
2126
+
2127
+ this._createAuditEntry('escalation_level_started', {
2128
+ level: level.levelNumber,
2129
+ timeout_ms: level.timeout,
2130
+ steps_count: levelSteps.length,
2131
+ severity: 'info'
2132
+ });
2133
+
1524
2134
  if (level.timeout === 0) {
1525
- const levelSteps = require('./parser').parseBlock(level.steps);
2135
+ // Immediate level — execute sequentially
1526
2136
  for (const levelStep of levelSteps) {
1527
2137
  await this.executeStep(levelStep, agentResolver);
1528
2138
  }
2139
+
2140
+ // Check if result was saved
1529
2141
  if (levelSteps.length > 0) {
1530
2142
  const lastStep = levelSteps[levelSteps.length - 1];
1531
2143
  if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
1532
2144
  finalResult = this.context[lastStep.saveAs];
1533
2145
  completedLevel = level.levelNumber;
1534
- break;
2146
+
2147
+ this._createAuditEntry('escalation_level_completed', {
2148
+ level: level.levelNumber,
2149
+ timed_out: false,
2150
+ severity: 'info'
2151
+ });
2152
+ break; // Escalation complete
1535
2153
  }
1536
2154
  }
2155
+
1537
2156
  } else {
1538
- currentTimeout += level.timeout;
1539
- const timeoutPromise = new Promise(resolve => {
1540
- setTimeout(() => resolve({ timedOut: true }), level.timeout);
1541
- });
2157
+ // Timed level
2158
+ const timeoutPromise = new Promise(resolve =>
2159
+ setTimeout(() => resolve({ timedOut: true }), level.timeout)
2160
+ );
2161
+
1542
2162
  const levelPromise = (async () => {
1543
- const levelSteps = require('./parser').parseBlock(level.steps);
1544
2163
  for (const levelStep of levelSteps) {
1545
2164
  await this.executeStep(levelStep, agentResolver);
1546
2165
  }
1547
2166
  return { timedOut: false };
1548
2167
  })();
2168
+
1549
2169
  const result = await Promise.race([timeoutPromise, levelPromise]);
2170
+
1550
2171
  if (!result.timedOut) {
1551
- if (levelSteps && levelSteps.length > 0) {
2172
+ // Level completed within time
2173
+ if (levelSteps.length > 0) {
1552
2174
  const lastStep = levelSteps[levelSteps.length - 1];
1553
2175
  if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
1554
2176
  finalResult = this.context[lastStep.saveAs];
1555
2177
  completedLevel = level.levelNumber;
1556
- break;
2178
+
2179
+ this._createAuditEntry('escalation_level_completed', {
2180
+ level: level.levelNumber,
2181
+ timed_out: false,
2182
+ severity: 'info'
2183
+ });
2184
+ break; // Escalation complete
1557
2185
  }
1558
2186
  }
2187
+ } else {
2188
+ // Level timed out
2189
+ this._createAuditEntry('escalation_level_timeout', {
2190
+ level: level.levelNumber,
2191
+ timeout_ms: level.timeout,
2192
+ severity: 'warn'
2193
+ });
2194
+
2195
+ if (this.verbose) {
2196
+ console.log(
2197
+ `⏰ Escalation level ${level.levelNumber} timed out ` +
2198
+ `after ${level.timeout}ms — trying next level`
2199
+ );
2200
+ }
2201
+ // Continue to next level
1559
2202
  }
1560
2203
  }
1561
2204
  }
2205
+
2206
+ // Final context state
1562
2207
  this.context.escalation_completed = finalResult !== null;
1563
2208
  this.context.timed_out = finalResult === null;
1564
2209
  if (completedLevel !== null) {
1565
2210
  this.context.escalation_level = completedLevel;
1566
2211
  }
2212
+
2213
+ this._createAuditEntry('escalation_outcome', {
2214
+ completed: finalResult !== null,
2215
+ completed_at_level: completedLevel,
2216
+ timed_out: finalResult === null,
2217
+ severity: finalResult !== null ? 'info' : 'warn'
2218
+ });
2219
+
1567
2220
  break;
1568
2221
  }
1569
-
1570
- case 'connect': {
1571
- this.resources[step.resource] = step.endpoint;
1572
- break;
1573
- }
1574
-
2222
+
2223
+ case 'connect': {
2224
+ if (!step.resource || !step.endpoint) {
2225
+ this.addWarning('Connect step missing "resource" or "endpoint". Skipping.');
2226
+ break;
2227
+ }
2228
+
2229
+ // Only validate URL format for url-type connects.
2230
+ // Resolver-type endpoints are package names (@o-lang/kyc-resolver)
2231
+ // and are not valid URLs — do not run them through new URL().
2232
+ if (!step.targetType || step.targetType === 'url') {
2233
+ try {
2234
+ new URL(step.endpoint);
2235
+ } catch (e) {
2236
+ this.addWarning(`Connect: Invalid endpoint URL for "${step.resource}": ${step.endpoint}`);
2237
+ break;
2238
+ }
2239
+ }
2240
+
2241
+ this.resources[step.resource] = step.endpoint;
2242
+
2243
+ this._createAuditEntry('resource_connected', {
2244
+ resource: step.resource,
2245
+ target_type: step.targetType || 'url',
2246
+ endpoint_masked: step.endpoint.replace(/\/\/[^@]+@/, '//***@'),
2247
+ severity: 'info'
2248
+ });
2249
+
2250
+ if (this.verbose) {
2251
+ console.log(`🔗 Connected "${step.resource}" → ${step.endpoint}`);
2252
+ }
2253
+ break;
2254
+ }
2255
+ // ─────────────────────────────────────────────────────────────────────────────
2256
+ // AGENT_USE
2257
+ // Maps a logical agent name (e.g., "support_bot") to a registered resource.
2258
+ // ─────────────────────────────────────────────────────────────────────────────
1575
2259
  case 'agent_use': {
2260
+ if (!step.logicalName || !step.resource) {
2261
+ this.addWarning('Agent_use step missing "logicalName" or "resource". Skipping.');
2262
+ break;
2263
+ }
2264
+
2265
+ // Optional: Validate that the resource was previously connected
2266
+ if (!this.resources[step.resource]) {
2267
+ this.addWarning(`Agent_use: Resource "${step.resource}" has not been connected yet.`);
2268
+ }
2269
+
1576
2270
  this.agentMap[step.logicalName] = step.resource;
2271
+
2272
+ // ✅ AUDIT LOG: Agent Mapping
2273
+ this._createAuditEntry('agent_mapped', {
2274
+ logical_name: step.logicalName,
2275
+ resource: step.resource,
2276
+ severity: 'info'
2277
+ });
2278
+
2279
+ if (this.verbose) {
2280
+ console.log(`🤖 Mapped agent "${step.logicalName}" to resource "${step.resource}"`);
2281
+ }
1577
2282
  break;
1578
2283
  }
1579
-
2284
+ // ─────────────────────────────────────────────────────────────────────────────
2285
+ // DEBRIEF
2286
+ //
2287
+ // Governance Features:
2288
+ // 1. Semantic Validation: Ensures all {symbols} exist before emitting.
2289
+ // 2. Interpolation: Agents receive resolved values, not raw templates.
2290
+ // 3. Auditability: Every debrief is logged for compliance tracing.
2291
+ // ─────────────────────────────────────────────────────────────────────────────
2292
+
1580
2293
  case 'debrief': {
1581
- if (step.message.includes('{')) {
1582
- const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
1583
- for (const symbolMatch of symbols) {
1584
- const symbol = symbolMatch.replace(/[{}]/g, '');
1585
- this._requireSemantic(symbol, 'debrief');
2294
+ const messageTemplate = step.message;
2295
+
2296
+ // 1. Validate all referenced symbols exist in context
2297
+ if (messageTemplate && messageTemplate.includes('{')) {
2298
+ const symbolMatches = messageTemplate.match(/\{([^\}]+)\}/g) || [];
2299
+
2300
+ // Check every symbol. _requireSemantic will emit 'semantic_violation'
2301
+ // if a symbol is missing, but we also need to stop execution here.
2302
+ const allPresent = symbolMatches.every(sym => {
2303
+ const key = sym.replace(/[{}]/g, '');
2304
+ return this._requireSemantic(key, 'debrief');
2305
+ });
2306
+
2307
+ if (!allPresent) {
2308
+ if (this.verbose) {
2309
+ console.log(`⏭️ Debrief skipped — one or more symbols missing in context`);
2310
+ }
2311
+ // Break early. Do not emit incomplete data.
2312
+ break;
1586
2313
  }
1587
2314
  }
1588
- this.emit('debrief', { agent: step.agent, message: step.message });
2315
+
2316
+ // 2. Interpolate — agent receives the real value, not the template
2317
+ const interpolatedMessage = this._safeInterpolate(
2318
+ messageTemplate,
2319
+ this.context,
2320
+ 'debrief message'
2321
+ );
2322
+
2323
+ // 3. Audit trail — every agent message must be traceable
2324
+ this._createAuditEntry('debrief_emitted', {
2325
+ agent: step.agent,
2326
+ message_length: interpolatedMessage ? interpolatedMessage.length : 0,
2327
+ symbols_resolved: (messageTemplate.match(/\{([^\}]+)\}/g) || []).length,
2328
+ severity: 'info'
2329
+ });
2330
+
2331
+ this.emit('debrief', {
2332
+ agent: step.agent,
2333
+ message: interpolatedMessage
2334
+ });
2335
+
2336
+ if (this.verbose) {
2337
+ console.log(`📨 Debrief → agent "${step.agent}": ${interpolatedMessage}`);
2338
+ }
1589
2339
  break;
1590
2340
  }
1591
-
2341
+
2342
+
1592
2343
  case 'prompt': {
1593
2344
  if (this.verbose) {
1594
2345
  console.log(`❓ Prompt: ${step.question}`);
1595
2346
  }
1596
2347
  break;
1597
2348
  }
1598
-
2349
+
2350
+ // ─────────────────────────────────────────────────────────────────────────────
2351
+ // EMIT
2352
+ //
2353
+ // Governance Features:
2354
+ // 1. Semantic Validation: Stops at first missing symbol to reduce noise.
2355
+ // 2. Auditability: External events are logged with payload metadata.
2356
+ // 3. Consistency: Uses same interpolation logic as debrief.
2357
+ // ─────────────────────────────────────────────────────────────────────────────
2358
+
1599
2359
  case 'emit': {
1600
2360
  const payloadTemplate = step.payload;
2361
+
2362
+ // Extract unique symbols from the payload template
1601
2363
  const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
1602
- let shouldEmit = true;
1603
- for (const symbolMatch of symbols) {
1604
- const symbol = symbolMatch.replace(/[{}]/g, '');
1605
- if (!this._requireSemantic(symbol, 'emit')) {
1606
- shouldEmit = false;
1607
- }
1608
- }
1609
- if (!shouldEmit) {
2364
+
2365
+ // Validate all symbols, stop at first missing one
2366
+ const allPresent = symbols.every(sym => {
2367
+ const key = sym.replace(/[{}]/g, '');
2368
+ return this._requireSemantic(key, 'emit');
2369
+ });
2370
+
2371
+ if (!allPresent) {
1610
2372
  if (this.verbose) {
1611
- console.log(`⏭️ Skipped emit due to missing semantic symbols`);
2373
+ console.log(`⏭️ Skipped emit due to missing semantic symbols`);
1612
2374
  }
1613
2375
  break;
1614
2376
  }
1615
- const payload = this._safeInterpolate(step.payload, this.context, 'emit payload');
2377
+
2378
+ // Interpolate the payload
2379
+ const payload = this._safeInterpolate(
2380
+ payloadTemplate,
2381
+ this.context,
2382
+ 'emit payload'
2383
+ );
2384
+
2385
+ // ✅ AUDIT LOG: External event emission
2386
+ this._createAuditEntry('event_emitted', {
2387
+ event: step.event,
2388
+ payload_length: payload ? payload.length : 0,
2389
+ symbols_resolved: symbols.length,
2390
+ severity: 'info'
2391
+ });
2392
+
1616
2393
  this.emit(step.event, {
1617
- payload: payload,
1618
- workflow: this.context.workflow_name,
2394
+ payload,
2395
+ workflow: this.context.workflow_name || 'unknown',
1619
2396
  timestamp: new Date().toISOString()
1620
2397
  });
2398
+
1621
2399
  if (this.verbose) {
1622
2400
  console.log(`📤 Emit event "${step.event}" with payload: ${payload}`);
1623
2401
  }
1624
2402
  break;
1625
2403
  }
1626
-
2404
+
2405
+ // ─────────────────────────────────────────────────────────────────────────────
2406
+ // PERSIST
2407
+ //
2408
+ // Governance Features:
2409
+ // 1. Data Integrity: Prevents silent corruption of objects into "[object Object]".
2410
+ // 2. Auditability: Logs path, format, and variable name (without raw values).
2411
+ // 3. Safety: Validates semantic existence before attempting I/O.
2412
+ // ─────────────────────────────────────────────────────────────────────────────
2413
+
1627
2414
  case 'persist': {
1628
- if (!this._requireSemantic(step.variable, 'persist')) {
2415
+ const fs = require('fs');
2416
+ const path = require('path');
2417
+
2418
+ // 1. Semantic Guard: Ensure the variable exists in context
2419
+ if (!step.variable || !this._requireSemantic(step.variable, 'persist')) {
1629
2420
  if (this.verbose) {
1630
- console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
2421
+ console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
1631
2422
  }
1632
2423
  break;
1633
2424
  }
2425
+
1634
2426
  const sourceValue = this.context[step.variable];
2427
+
2428
+ // Resolve absolute path to prevent directory traversal attacks or ambiguity
1635
2429
  const outputPath = path.resolve(process.cwd(), step.target);
1636
2430
  const outputDir = path.dirname(outputPath);
2431
+
2432
+ // Ensure directory exists
1637
2433
  if (!fs.existsSync(outputDir)) {
1638
- fs.mkdirSync(outputDir, { recursive: true });
2434
+ try {
2435
+ fs.mkdirSync(outputDir, { recursive: true });
2436
+ } catch (e) {
2437
+ this.addWarning(`persist: failed to create directory "${outputDir}": ${e.message}`);
2438
+ break;
2439
+ }
1639
2440
  }
2441
+
2442
+ // 2. Serialize with safe fallback for object → non-JSON targets
1640
2443
  let content;
2444
+ const isObject = sourceValue !== null && typeof sourceValue === 'object';
2445
+ let formatUsed = 'string';
2446
+
1641
2447
  if (step.target.endsWith('.json')) {
2448
+ // Standard JSON serialization
1642
2449
  content = JSON.stringify(sourceValue, null, 2);
2450
+ formatUsed = 'json';
2451
+ } else if (isObject) {
2452
+ // CRITICAL FIX: Prevents "[object Object]" data corruption.
2453
+ // If user tries to save an object to .txt/.csv, we coerce to JSON
2454
+ // so the data remains recoverable, but we warn them.
2455
+ this.addWarning(
2456
+ `persist: "${step.variable}" is an object but target "${step.target}" is not .json. ` +
2457
+ `Writing as JSON to prevent data loss. Rename target to .json or select a specific field.`
2458
+ );
2459
+ content = JSON.stringify(sourceValue, null, 2);
2460
+ formatUsed = 'json-fallback';
1643
2461
  } else {
2462
+ // Primitive values (string, number, boolean)
1644
2463
  content = String(sourceValue);
2464
+ formatUsed = 'string';
2465
+ }
2466
+
2467
+ // 3. Write with error surfacing
2468
+ try {
2469
+ fs.writeFileSync(outputPath, content, 'utf8');
2470
+ } catch (e) {
2471
+ this.addWarning(`persist: failed to write "${step.variable}" to "${step.target}": ${e.message}`);
2472
+
2473
+ // ✅ AUDIT LOG: Failed Write
2474
+ this._createAuditEntry('persist_failed', {
2475
+ variable: step.variable,
2476
+ target: step.target,
2477
+ error: e.message,
2478
+ severity: 'high'
2479
+ });
2480
+ break;
1645
2481
  }
1646
- fs.writeFileSync(outputPath, content, 'utf8');
2482
+
2483
+ // 4. ✅ AUDIT LOG: Successful Write
2484
+ // Note: We do NOT log the actual value content to protect PII.
2485
+ // We log metadata only.
2486
+ this._createAuditEntry('context_persisted', {
2487
+ variable: step.variable,
2488
+ target: step.target,
2489
+ format: formatUsed,
2490
+ value_type: isObject ? 'object' : typeof sourceValue,
2491
+ byte_length: Buffer.byteLength(content, 'utf8'),
2492
+ severity: 'info'
2493
+ });
2494
+
1647
2495
  if (this.verbose) {
1648
- console.log(`💾 Persisted "${step.variable}" to ${step.target}`);
2496
+ console.log(`💾 Persisted "${step.variable}" to ${step.target} (${formatUsed})`);
1649
2497
  }
1650
2498
  break;
1651
2499
  }
1652
-
2500
+
1653
2501
  case 'persist-db': {
1654
2502
  if (!this.dbClient) {
1655
2503
  this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
@@ -1682,7 +2530,7 @@ _unwrapResolverResult(result) {
1682
2530
  const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
1683
2531
  await db.collection(step.collection).insertOne({
1684
2532
  workflow_name: this.context.workflow_name || 'unknown',
1685
- sourceValue,
2533
+ sourceValue,
1686
2534
  created_at: new Date()
1687
2535
  });
1688
2536
  break;
@@ -1717,18 +2565,20 @@ _unwrapResolverResult(result) {
1717
2565
  if (workflow.type !== 'workflow') {
1718
2566
  throw new Error(`Unknown workflow type: ${workflow.type}`);
1719
2567
  }
1720
-
2568
+
1721
2569
  this.context = {
1722
2570
  ...inputs,
1723
2571
  workflow_name: workflow.name
1724
2572
  };
1725
2573
 
1726
- // ✅ NEW: Validate Inputs BEFORE any step runs
1727
- this._validateInputs(inputs);
2574
+ // ✅ NEW: Validate Inputs BEFORE any step runs
2575
+ // In redact mode: PII tokens are replaced in place, execution continues.
2576
+ // In block mode (default): throws on any PII or financial intent match.
2577
+ this._validateInputs(inputs);
1728
2578
 
1729
2579
  // ✅ AUDIT LOG: Workflow start (ENHANCED with governance metadata)
1730
2580
  const governanceHash = this._generateGovernanceProfileHash(workflow);
1731
-
2581
+
1732
2582
  this._createAuditEntry('workflow_started', {
1733
2583
  workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`, // ✅ Workflow ID
1734
2584
  workflow_name: workflow.name,
@@ -1739,6 +2589,7 @@ _unwrapResolverResult(result) {
1739
2589
  inputs_count: Object.keys(inputs).length,
1740
2590
  steps_count: workflow.steps.length,
1741
2591
  allowed_resolvers: workflow.allowedResolvers || [],
2592
+ pii_mode: this.piiMode, // ✅ NEW v1.3.0 — surfaced in audit
1742
2593
  constraints: {
1743
2594
  max_generations: workflow.maxGenerations,
1744
2595
  strict_inputs: process.env.OLANG_STRICT_INPUTS === 'true'
@@ -1773,7 +2624,7 @@ _unwrapResolverResult(result) {
1773
2624
  for (const step of workflow.steps) {
1774
2625
  await this.executeStep(step, agentResolver);
1775
2626
  }
1776
-
2627
+
1777
2628
  // ✅ AUDIT LOG: Workflow completion (ENHANCED)
1778
2629
  this._createAuditEntry('workflow_completed', {
1779
2630
  workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`,