@o-lang/olang 1.2.37 → 1.3.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -11
- package/src/runtime/RuntimeAPI.js +684 -391
- package/src/runtime/index.js +11 -1
|
@@ -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.
|
|
6
|
+
const KERNEL_VERSION = '1.3.0-alpha'; // 🔁 Bumped: PII redaction engine added
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// ✅ NEW v1.3.0 — SEPARATED PATTERN SETS
|
|
10
|
+
//
|
|
11
|
+
// WHY: Previously _validateInputs and _validateLLMOutput both duplicated one
|
|
12
|
+
// giant flat list. PII redaction must ONLY replace PII tokens (phone numbers,
|
|
13
|
+
// BVNs, account numbers) — it must NOT replace financial intent phrases like
|
|
14
|
+
// "fi owo ranṣẹ" with "[TRANSFER_REDACTED]" in legitimate LLM prompts.
|
|
15
|
+
//
|
|
16
|
+
// Backward compatibility: all old patterns are preserved exactly. They are now
|
|
17
|
+
// organised into two methods:
|
|
18
|
+
// _getPIIPatterns() → used by new _redactPII()
|
|
19
|
+
// _getFinancialIntentPatterns() → used by _validateInputs / _validateLLMOutput
|
|
20
|
+
//
|
|
21
|
+
// BACKWARD COMPAT: _validateInputs still throws by default (MODE = 'block').
|
|
22
|
+
// Set OLANG_PII_MODE=redact in env to switch to non-throwing redaction mode.
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
24
|
|
|
8
25
|
class RuntimeAPI {
|
|
9
26
|
constructor({ verbose = false } = {}) {
|
|
@@ -20,24 +37,362 @@ class RuntimeAPI {
|
|
|
20
37
|
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
|
21
38
|
this.disallowedLogFile = path.join(logsDir, 'disallowed_resolvers.json');
|
|
22
39
|
this.disallowedAttempts = [];
|
|
23
|
-
|
|
40
|
+
|
|
24
41
|
// ✅ NEW: Database client setup
|
|
25
42
|
this.dbClient = null;
|
|
26
43
|
this._initDbClient();
|
|
27
|
-
|
|
44
|
+
|
|
28
45
|
// ✅ NEW: Cryptographically verifiable audit logs
|
|
29
46
|
this.auditLog = [];
|
|
30
47
|
this.previousHash = 'GENESIS';
|
|
31
48
|
this.auditLogPrivateKey = process.env.OLANG_AUDIT_PRIVATE_KEY;
|
|
32
49
|
this.auditLogFile = path.join(logsDir, 'audit_log.json');
|
|
33
50
|
this.enableAuditLog = process.env.OLANG_AUDIT_LOG === 'true';
|
|
34
|
-
|
|
51
|
+
|
|
52
|
+
// ✅ NEW v1.3.0 — PII operating mode
|
|
53
|
+
// 'block' → original behaviour: throw on PII (default, backward compat)
|
|
54
|
+
// 'redact' → new behaviour: replace PII tokens, continue execution
|
|
55
|
+
// 'redact-and-log' → redact + emit audit entry per redaction event
|
|
56
|
+
this.piiMode = process.env.OLANG_PII_MODE || 'block';
|
|
57
|
+
|
|
35
58
|
if (this.enableAuditLog && this.verbose) {
|
|
36
59
|
console.log('🔐 Cryptographically verifiable audit logging enabled');
|
|
37
60
|
}
|
|
61
|
+
|
|
62
|
+
if (this.verbose && this.piiMode !== 'block') {
|
|
63
|
+
console.log(`🛡️ PII mode: ${this.piiMode}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ================================
|
|
68
|
+
// ✅ NEW v1.3.0 — PII-ONLY PATTERN SET
|
|
69
|
+
//
|
|
70
|
+
// These patterns match concrete identifiers that can be replaced with a
|
|
71
|
+
// [TYPE_REDACTED] token without destroying the semantic meaning of a sentence.
|
|
72
|
+
// They cover every language listed on the O-Lang site.
|
|
73
|
+
// ================================
|
|
74
|
+
|
|
75
|
+
_getPIIPatterns() {
|
|
76
|
+
return [
|
|
77
|
+
|
|
78
|
+
// ── Phone Numbers ──────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
// Nigeria (MTN, Airtel, Glo, 9mobile)
|
|
81
|
+
{
|
|
82
|
+
pattern: /\b(?:\+?234\s*[-.]?|0)(?:70|80|81|90|91)\d{8}\b/g,
|
|
83
|
+
capability: 'pii_phone',
|
|
84
|
+
lang: 'ng',
|
|
85
|
+
label: 'NG_PHONE'
|
|
86
|
+
},
|
|
87
|
+
// Kenya (+254 / 07xx / 01xx)
|
|
88
|
+
{
|
|
89
|
+
pattern: /\b(?:\+?254\s*[-.]?|0)(?:7[0-9]|1[01])\d{7}\b/g,
|
|
90
|
+
capability: 'pii_phone',
|
|
91
|
+
lang: 'ke',
|
|
92
|
+
label: 'KE_PHONE'
|
|
93
|
+
},
|
|
94
|
+
// South Africa (+27 / 0xx)
|
|
95
|
+
{
|
|
96
|
+
pattern: /\b(?:\+?27\s*[-.]?|0)[6-8]\d{8}\b/g,
|
|
97
|
+
capability: 'pii_phone',
|
|
98
|
+
lang: 'za',
|
|
99
|
+
label: 'ZA_PHONE'
|
|
100
|
+
},
|
|
101
|
+
// Ethiopia (+251)
|
|
102
|
+
{
|
|
103
|
+
pattern: /\b(?:\+?251\s*[-.]?|0)[79]\d{8}\b/g,
|
|
104
|
+
capability: 'pii_phone',
|
|
105
|
+
lang: 'et',
|
|
106
|
+
label: 'ET_PHONE'
|
|
107
|
+
},
|
|
108
|
+
// 🇬🇭 Ghana (+233) - MTN (24/54), Telecel (20/50), AT (26/56), Glo (23)
|
|
109
|
+
{
|
|
110
|
+
pattern: /\b(?:\+?233\s*[-.]?|0)(?:2[0346]|5[046])\d{7}\b/g,
|
|
111
|
+
capability: 'pii_phone',
|
|
112
|
+
lang: 'gh',
|
|
113
|
+
label: 'GH_PHONE'
|
|
114
|
+
},
|
|
115
|
+
// Generic international E.164
|
|
116
|
+
{
|
|
117
|
+
pattern: /\+(?!234|254|27\b|251|233)[1-9]\d{1,2}[-.\s]?\d{3,5}[-.\s]?\d{4,9}\b/g,
|
|
118
|
+
capability: 'pii_phone',
|
|
119
|
+
lang: 'intl',
|
|
120
|
+
label: 'INTL_PHONE'
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// ── National Identity Numbers ──────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
// Nigeria BVN (11 digits)
|
|
126
|
+
{
|
|
127
|
+
pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/ig,
|
|
128
|
+
capability: 'pii_national_id',
|
|
129
|
+
lang: 'ng',
|
|
130
|
+
label: 'NG_BVN'
|
|
131
|
+
},
|
|
132
|
+
// Nigeria NIN (11 digits)
|
|
133
|
+
{
|
|
134
|
+
pattern: /\b(?:nin|national\s+identification\s+number)\b.{0,20}\d{11}/ig,
|
|
135
|
+
capability: 'pii_national_id',
|
|
136
|
+
lang: 'ng',
|
|
137
|
+
label: 'NG_NIN'
|
|
138
|
+
},
|
|
139
|
+
// 🇬🇭 Ghana Card (National ID) - Format: GHA-XXXXXXXXX-X
|
|
140
|
+
{
|
|
141
|
+
pattern: /\bGHA-\d{9}-\d\b/ig,
|
|
142
|
+
capability: 'pii_national_id',
|
|
143
|
+
lang: 'gh',
|
|
144
|
+
label: 'GH_CARD'
|
|
145
|
+
},
|
|
146
|
+
// South Africa ID (13 digits YYMMDD + gender + race + check)
|
|
147
|
+
{
|
|
148
|
+
pattern: /\b[0-9]{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01])[0-9]{4}[01][0-9]{2}\b/g,
|
|
149
|
+
capability: 'pii_national_id',
|
|
150
|
+
lang: 'za',
|
|
151
|
+
label: 'ZA_ID'
|
|
152
|
+
},
|
|
153
|
+
// Kenya Huduma / National ID (7-8 digits)
|
|
154
|
+
{
|
|
155
|
+
pattern: /\b(?:national\s+id|id\s+number|huduma\s+namba)\b.{0,10}\d{7,8}\b/ig,
|
|
156
|
+
capability: 'pii_national_id',
|
|
157
|
+
lang: 'ke',
|
|
158
|
+
label: 'KE_ID'
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// ── Bank Account Numbers ───────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
// Generic account reference (works across all listed languages)
|
|
164
|
+
// Added Twi/Akan terms: 'sika' (money), 'konte' (account)
|
|
165
|
+
{
|
|
166
|
+
pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#|compte|cuenta|konto|konte|sika\s+number|حساب|حساب\s+رقم|حسابي|akaunti\s+ya|nambari\s+ya\s+akaunti)\s*[:\-—–]?\s*(\d{6,18})\b/ig,
|
|
167
|
+
capability: 'pii_account',
|
|
168
|
+
lang: 'multi',
|
|
169
|
+
label: 'ACCOUNT_NUMBER'
|
|
170
|
+
},
|
|
171
|
+
// IBAN (EU + Africa SWIFT members)
|
|
172
|
+
{
|
|
173
|
+
pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g,
|
|
174
|
+
capability: 'pii_account',
|
|
175
|
+
lang: 'intl',
|
|
176
|
+
label: 'IBAN'
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// ── Email Addresses ────────────────────────────────────────────────────
|
|
180
|
+
{
|
|
181
|
+
pattern: /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/g,
|
|
182
|
+
capability: 'pii_email',
|
|
183
|
+
lang: 'multi',
|
|
184
|
+
label: 'EMAIL'
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// ── Deceptive completion claims (cross-language) ───────────────────────
|
|
188
|
+
// These belong in PII set because they mask fraudulent state
|
|
189
|
+
{
|
|
190
|
+
pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|verified|imethibitishwa|imefanikiwa|amthibitishwa|ti\s+da|ti\s+ṣe|gụnyere|kimefanyika|yamekamilika)\b/ig,
|
|
191
|
+
capability: 'pii_deceptive_claim',
|
|
192
|
+
lang: 'multi',
|
|
193
|
+
label: 'DECEPTIVE_CLAIM'
|
|
194
|
+
}
|
|
195
|
+
];
|
|
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,7 +997,7 @@ class RuntimeAPI {
|
|
|
638
997
|
return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
|
|
639
998
|
}
|
|
640
999
|
|
|
641
|
-
|
|
1000
|
+
evaluateCondition(cond, ctx) {
|
|
642
1001
|
cond = cond.trim();
|
|
643
1002
|
|
|
644
1003
|
// ✅ 1. Handle Logical OR (|| or 'or')
|
|
@@ -670,6 +1029,7 @@ class RuntimeAPI {
|
|
|
670
1029
|
// Fallback: truthy check
|
|
671
1030
|
return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
|
|
672
1031
|
}
|
|
1032
|
+
|
|
673
1033
|
mathFunctions = {
|
|
674
1034
|
add: (a, b) => a + b,
|
|
675
1035
|
subtract: (a, b) => a - b,
|
|
@@ -690,38 +1050,38 @@ class RuntimeAPI {
|
|
|
690
1050
|
abs: a => Math.abs(a)
|
|
691
1051
|
};
|
|
692
1052
|
|
|
693
|
-
|
|
1053
|
+
evaluateMath(expr) {
|
|
694
1054
|
// ✅ Handle quoted string literals with interpolation: "{var}" → interpolated string
|
|
695
1055
|
if (typeof expr === 'string') {
|
|
696
1056
|
const trimmed = expr.trim();
|
|
697
|
-
|
|
1057
|
+
|
|
698
1058
|
// Check if it's a quoted string (single or double quotes)
|
|
699
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
1059
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
700
1060
|
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
701
1061
|
// Extract the inner content
|
|
702
1062
|
let inner = trimmed.slice(1, -1);
|
|
703
|
-
|
|
1063
|
+
|
|
704
1064
|
// Perform interpolation: replace {var} with context values
|
|
705
1065
|
inner = inner.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
706
1066
|
const value = this.getNested(this.context, path.trim());
|
|
707
1067
|
return value !== undefined ? String(value) : `{${path}}`;
|
|
708
1068
|
});
|
|
709
|
-
|
|
1069
|
+
|
|
710
1070
|
return inner;
|
|
711
1071
|
}
|
|
712
1072
|
}
|
|
713
|
-
|
|
1073
|
+
|
|
714
1074
|
// ── Original math evaluation logic (unchanged) ──────────────────────────
|
|
715
1075
|
expr = expr.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
716
1076
|
const value = this.getNested(this.context, path.trim());
|
|
717
1077
|
if (typeof value === 'string') return `"${value.replace(/"/g, '\\"')}"`;
|
|
718
1078
|
return value !== undefined ? value : 0;
|
|
719
1079
|
});
|
|
720
|
-
|
|
1080
|
+
|
|
721
1081
|
const funcNames = Object.keys(this.mathFunctions);
|
|
722
1082
|
const safeFunc = {};
|
|
723
1083
|
funcNames.forEach(fn => safeFunc[fn] = this.mathFunctions[fn]);
|
|
724
|
-
|
|
1084
|
+
|
|
725
1085
|
try {
|
|
726
1086
|
const f = new Function(...funcNames, `return ${expr};`);
|
|
727
1087
|
return f(...funcNames.map(fn => safeFunc[fn]));
|
|
@@ -765,334 +1125,264 @@ class RuntimeAPI {
|
|
|
765
1125
|
});
|
|
766
1126
|
}
|
|
767
1127
|
|
|
768
|
-
//
|
|
769
|
-
// ✅ KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety)
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1128
|
+
// ================================
|
|
1129
|
+
// ✅ UPDATED v1.3.0 — KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety)
|
|
1130
|
+
//
|
|
1131
|
+
// BACKWARD COMPAT:
|
|
1132
|
+
// OLANG_PII_MODE=block (default) → original behaviour, throws on any match
|
|
1133
|
+
// OLANG_PII_MODE=redact → replaces PII tokens, continues
|
|
1134
|
+
// OLANG_PII_MODE=redact-and-log → redact + audit entry per redaction event
|
|
1135
|
+
//
|
|
1136
|
+
// Financial intent patterns always throw regardless of PII mode.
|
|
1137
|
+
// ================================
|
|
1138
|
+
_validateInputs(inputs) {
|
|
1139
|
+
// Only scan specific input fields that contain user text
|
|
1140
|
+
// NOTE: 'document_text' intentionally excluded — legal documents legitimately
|
|
1141
|
+
// contain financial terms and must not be blocked at the input layer.
|
|
1142
|
+
const fieldsToScan = ['user_message', 'user_question', 'text', 'prompt'];
|
|
1143
|
+
|
|
1144
|
+
const redactMode = this.piiMode === 'redact' || this.piiMode === 'redact-and-log';
|
|
1145
|
+
const allRedactions = {};
|
|
1146
|
+
|
|
1147
|
+
// ── PASS 1: PII redaction (when mode allows) ──────────────────────────
|
|
1148
|
+
if (redactMode) {
|
|
1149
|
+
for (const field of fieldsToScan) {
|
|
1150
|
+
const text = inputs[field];
|
|
1151
|
+
if (!text || typeof text !== 'string') continue;
|
|
1152
|
+
|
|
1153
|
+
const { redacted, redactions, wasModified } = this._redactPII(text);
|
|
1154
|
+
|
|
1155
|
+
if (wasModified) {
|
|
1156
|
+
inputs[field] = redacted; // mutate in place — caller sees clean value
|
|
1157
|
+
allRedactions[field] = redactions;
|
|
1158
|
+
|
|
1159
|
+
if (this.piiMode === 'redact-and-log') {
|
|
1160
|
+
this._createAuditEntry('pii_redacted', {
|
|
1161
|
+
field,
|
|
1162
|
+
redaction_count: redactions.length,
|
|
1163
|
+
redactions: redactions.map(r => ({
|
|
1164
|
+
label: r.replacement,
|
|
1165
|
+
capability: r.capability,
|
|
1166
|
+
lang: r.lang
|
|
1167
|
+
// NOTE: original value intentionally excluded from audit log
|
|
1168
|
+
// to avoid persisting the very PII we just redacted
|
|
1169
|
+
})),
|
|
1170
|
+
severity: 'info'
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
811
1173
|
|
|
1174
|
+
if (this.verbose) {
|
|
1175
|
+
console.log(
|
|
1176
|
+
`🛡️ [O-Lang PII] Redacted ${redactions.length} item(s) in "${field}": ` +
|
|
1177
|
+
redactions.map(r => r.replacement).join(', ')
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
812
1183
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
{ pattern: /tinye\s+(?:ego|moni|isi)/i, capability: 'deposit', lang: 'ig' },
|
|
818
|
-
{ pattern: /(?:ziri|bururu|tinyere|gbara)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
819
|
-
{ pattern: /m\s+(?:ziri|buru|zipuru|tinyere)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
1184
|
+
// ── PASS 2: Financial intent scan (always blocks, mode-independent) ───
|
|
1185
|
+
for (const field of fieldsToScan) {
|
|
1186
|
+
const text = inputs[field]; // may already be PII-redacted from pass 1
|
|
1187
|
+
if (!text || typeof text !== 'string') continue;
|
|
820
1188
|
|
|
821
|
-
//
|
|
822
|
-
{ pattern
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
{ pattern: /ongez[ae]?\s*(?:kiasi|pesa|fedha)/i, capability: 'deposit', lang: 'sw' },
|
|
828
|
-
{ pattern: /wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa)/i, capability: 'deposit', lang: 'sw' },
|
|
829
|
-
{ pattern: /nime(?:tuma|lipa|ongeza|weka|peleka)/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
1189
|
+
// 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION (INPUT)
|
|
1190
|
+
for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
|
|
1191
|
+
if (pattern.test(text)) {
|
|
1192
|
+
const match = text.match(pattern);
|
|
1193
|
+
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
|
|
1194
|
+
const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
|
|
830
1195
|
|
|
1196
|
+
// ✅ DECOUPLED: Check legal context via standardized signals (not UI fields)
|
|
1197
|
+
const intent = this.context.__verified_intent || {};
|
|
1198
|
+
const signals = intent.context_signals || {};
|
|
1199
|
+
|
|
1200
|
+
const isLegalContext =
|
|
1201
|
+
// Signal 1: Explicit scope declaration
|
|
1202
|
+
intent.scope === 'legal_analysis_only' ||
|
|
1203
|
+
|
|
1204
|
+
// Signal 2: Standardized context signals (server-mapped, UI-agnostic)
|
|
1205
|
+
signals.isLegalDocument === true ||
|
|
1206
|
+
signals.documentCategory === 'contract' ||
|
|
1207
|
+
signals.documentCategory === 'nda' ||
|
|
1208
|
+
signals.documentCategory === 'agreement' ||
|
|
1209
|
+
signals.documentCategory === 'legal' ||
|
|
1210
|
+
|
|
1211
|
+
// Signal 3: Semantic fallback (works even if signals missing)
|
|
1212
|
+
(typeof text === 'string' && /clause|term|agreement|contract|obligation|penalty|damages|breach|party|shall|herein/i.test(text));
|
|
1213
|
+
|
|
1214
|
+
// ✅ NEW: Check contextual allowlist if in legal context
|
|
1215
|
+
if (isLegalContext && this.context.__verified_intent?.contextual_allowlist) {
|
|
1216
|
+
const allowlist = this.context.__verified_intent.contextual_allowlist;
|
|
1217
|
+
const triggerWord = match ? match[0].toLowerCase() : '';
|
|
1218
|
+
|
|
1219
|
+
const allowed = allowlist.some(rule => {
|
|
1220
|
+
// Check if this pattern's capability matches the rule's trigger
|
|
1221
|
+
const triggerMatch =
|
|
1222
|
+
triggerWord.includes(rule.trigger.toLowerCase()) ||
|
|
1223
|
+
capability.toLowerCase().includes(rule.trigger.toLowerCase()); // ← Handle capability-level triggers
|
|
1224
|
+
|
|
1225
|
+
if (triggerMatch) {
|
|
1226
|
+
// Check if required legal keywords are present
|
|
1227
|
+
return rule.requires.some(keyword =>
|
|
1228
|
+
text.toLowerCase().includes(keyword.toLowerCase())
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
return false;
|
|
1232
|
+
});
|
|
831
1233
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
{ pattern: /thumel.*imali/i, capability: 'transfer', lang: 'zu' },
|
|
846
|
-
{ pattern: /hlawul/i, capability: 'payment', lang: 'zu' }, // Matches root inside hlawula, ngihlawule
|
|
847
|
-
{ pattern: /hlawul.*imali/i, capability: 'payment', lang: 'zu' },
|
|
1234
|
+
if (allowed) {
|
|
1235
|
+
// ✅ AUDIT LOG: Contextual allowlist bypass
|
|
1236
|
+
this._createAuditEntry('safety_bypass', {
|
|
1237
|
+
type: 'contextual_allowlist',
|
|
1238
|
+
trigger: triggerWord,
|
|
1239
|
+
legal_context: true,
|
|
1240
|
+
matched_keywords: this.context.__verified_intent.contextual_allowlist
|
|
1241
|
+
.find(r => triggerWord.includes(r.trigger.toLowerCase()))?.requires || [],
|
|
1242
|
+
severity: 'info'
|
|
1243
|
+
});
|
|
1244
|
+
continue; // Skip blocking this match
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
848
1247
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1248
|
+
// ✅ AUDIT LOG: Input Safety Violation (only if not bypassed)
|
|
1249
|
+
this._createAuditEntry('input_safety_violation', {
|
|
1250
|
+
type: 'blocked_input',
|
|
1251
|
+
field: field,
|
|
1252
|
+
detected_phrase: match ? match[0].trim() : 'unknown pattern',
|
|
1253
|
+
capability: capability,
|
|
1254
|
+
language: lang,
|
|
1255
|
+
african_language_detected: isAfrican,
|
|
1256
|
+
financial_expression_found: isFinancial,
|
|
1257
|
+
legal_context_detected: false,
|
|
1258
|
+
severity: 'high'
|
|
1259
|
+
});
|
|
857
1260
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1261
|
+
throw new Error(
|
|
1262
|
+
`[O-Lang SAFETY] Blocked Input in "${lang}":\n` +
|
|
1263
|
+
` → Detected: "${match ? match[0].trim() : 'Pattern Match'}"\n` +
|
|
1264
|
+
` → Capability: ${capability}\n` +
|
|
1265
|
+
` → Field: ${field}\n` +
|
|
1266
|
+
` → African Language Detected: ${isAfrican}\n` +
|
|
1267
|
+
` → Financial Expression: ${isFinancial}\n` +
|
|
1268
|
+
`\n🛑 Workflow halted before execution.`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
866
1273
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
// Check if required legal keywords are present
|
|
889
|
-
return rule.requires.some(keyword =>
|
|
890
|
-
text.toLowerCase().includes(keyword.toLowerCase())
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
return false;
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
if (allowed) {
|
|
897
|
-
// ✅ AUDIT LOG: Contextual allowlist bypass
|
|
898
|
-
this._createAuditEntry('safety_bypass', {
|
|
899
|
-
type: 'contextual_allowlist',
|
|
900
|
-
trigger: triggerWord,
|
|
901
|
-
legal_context: true,
|
|
902
|
-
matched_keywords: this.context.__verified_intent.contextual_allowlist
|
|
903
|
-
.find(r => triggerWord.includes(r.trigger.toLowerCase()))?.requires || [],
|
|
904
|
-
severity: 'info'
|
|
1274
|
+
// ── PASS 3: PII block scan (only in block mode — original behaviour) ──
|
|
1275
|
+
if (!redactMode) {
|
|
1276
|
+
for (const field of fieldsToScan) {
|
|
1277
|
+
const text = inputs[field];
|
|
1278
|
+
if (!text || typeof text !== 'string') continue;
|
|
1279
|
+
|
|
1280
|
+
for (const { pattern, capability, lang, label } of this._getPIIPatterns()) {
|
|
1281
|
+
if (pattern.test(text)) {
|
|
1282
|
+
const match = text.match(pattern);
|
|
1283
|
+
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
|
|
1284
|
+
|
|
1285
|
+
this._createAuditEntry('input_safety_violation', {
|
|
1286
|
+
type: 'blocked_input',
|
|
1287
|
+
field: field,
|
|
1288
|
+
detected_phrase: match ? match[0].trim() : 'unknown pattern',
|
|
1289
|
+
capability: capability,
|
|
1290
|
+
language: lang,
|
|
1291
|
+
african_language_detected: isAfrican,
|
|
1292
|
+
financial_expression_found: false,
|
|
1293
|
+
pii_type: label,
|
|
1294
|
+
severity: 'high'
|
|
905
1295
|
});
|
|
906
|
-
|
|
1296
|
+
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
`[O-Lang SAFETY] Blocked PII in field "${field}" (${lang}):\n` +
|
|
1299
|
+
` → Type: ${label}\n` +
|
|
1300
|
+
` → Capability: ${capability}\n` +
|
|
1301
|
+
`\n🛑 Workflow halted before execution. Set OLANG_PII_MODE=redact to auto-redact instead.`
|
|
1302
|
+
);
|
|
907
1303
|
}
|
|
908
1304
|
}
|
|
909
|
-
|
|
910
|
-
// ✅ AUDIT LOG: Input Safety Violation (only if not bypassed)
|
|
911
|
-
this._createAuditEntry('input_safety_violation', {
|
|
912
|
-
type: 'blocked_input',
|
|
913
|
-
field: field,
|
|
914
|
-
detected_phrase: match ? match[0].trim() : 'unknown pattern',
|
|
915
|
-
capability: capability,
|
|
916
|
-
language: lang,
|
|
917
|
-
african_language_detected: isAfrican,
|
|
918
|
-
financial_expression_found: isFinancial,
|
|
919
|
-
legal_context_detected: isLegalContext,
|
|
920
|
-
severity: 'high'
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
throw new Error(
|
|
924
|
-
`[O-Lang SAFETY] Blocked Input in "${lang}":\n` +
|
|
925
|
-
` → Detected: "${match ? match[0].trim() : 'Pattern Match'}"\n` +
|
|
926
|
-
` → Capability: ${capability}\n` +
|
|
927
|
-
` → Field: ${field}\n` +
|
|
928
|
-
` → African Language Detected: ${isAfrican}\n` +
|
|
929
|
-
` → Financial Expression: ${isFinancial}\n` +
|
|
930
|
-
` → Legal Context: ${isLegalContext}\n` +
|
|
931
|
-
`\n🛑 Workflow halted before execution.`
|
|
932
|
-
);
|
|
933
1305
|
}
|
|
934
1306
|
}
|
|
1307
|
+
|
|
1308
|
+
return {
|
|
1309
|
+
passed: true,
|
|
1310
|
+
redactions: Object.keys(allRedactions).length > 0 ? allRedactions : null
|
|
1311
|
+
};
|
|
935
1312
|
}
|
|
936
|
-
return { passed: true };
|
|
937
|
-
}
|
|
938
1313
|
|
|
939
|
-
//
|
|
940
|
-
// ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION
|
|
941
|
-
//
|
|
1314
|
+
// ================================
|
|
1315
|
+
// ✅ UPDATED v1.3.0 — KERNEL-LEVEL LLM HALLUCINATION PREVENTION
|
|
1316
|
+
// (CONJUGATION-AWARE + EVASION-RESISTANT)
|
|
1317
|
+
//
|
|
1318
|
+
// Now uses _getFinancialIntentPatterns() instead of inline duplicate.
|
|
1319
|
+
// Xhosa patterns included automatically via the shared set.
|
|
1320
|
+
// All __verified_intent logic unchanged for backward compat.
|
|
1321
|
+
// ================================
|
|
942
1322
|
_validateLLMOutput(output, actionContext) {
|
|
943
1323
|
if (!output || typeof output !== 'string') return { passed: true };
|
|
944
1324
|
|
|
945
1325
|
// ── __verified_intent takes priority ──────────────────────────────────────
|
|
946
|
-
const intent = this.context.__verified_intent;
|
|
947
|
-
if (intent) {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1326
|
+
const intent = this.context.__verified_intent;
|
|
1327
|
+
if (intent) {
|
|
1328
|
+
if (intent.prohibited_actions && Array.isArray(intent.prohibited_actions)) {
|
|
1329
|
+
const lower = output.toLowerCase();
|
|
1330
|
+
for (const action of intent.prohibited_actions) {
|
|
1331
|
+
if (lower.includes(action.toLowerCase())) {
|
|
1332
|
+
return {
|
|
1333
|
+
passed: false,
|
|
1334
|
+
reason: `Output violates prohibited action "${action}" defined in __verified_intent`,
|
|
1335
|
+
detected: action,
|
|
1336
|
+
language: 'multi'
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
958
1340
|
}
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
1341
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1342
|
+
if (intent.prohibited_topics && Array.isArray(intent.prohibited_topics)) {
|
|
1343
|
+
for (const topic of intent.prohibited_topics) {
|
|
1344
|
+
const isRegex = typeof topic === 'object' && topic.pattern;
|
|
1345
|
+
let matched = false;
|
|
1346
|
+
let detected = '';
|
|
1347
|
+
|
|
1348
|
+
if (isRegex) {
|
|
1349
|
+
try {
|
|
1350
|
+
const re = new RegExp(topic.pattern, topic.flags || 'i');
|
|
1351
|
+
const match = output.match(re);
|
|
1352
|
+
matched = !!match;
|
|
1353
|
+
detected = match ? match[0] : topic.pattern;
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
this.addWarning(`Invalid prohibited_topic regex: "${topic.pattern}" — ${e.message}`);
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
} else {
|
|
1359
|
+
matched = output.toLowerCase().includes(topic.toLowerCase());
|
|
1360
|
+
detected = topic;
|
|
1361
|
+
}
|
|
967
1362
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
continue;
|
|
1363
|
+
if (matched) {
|
|
1364
|
+
return {
|
|
1365
|
+
passed: false,
|
|
1366
|
+
reason: `Output violates prohibited topic "${isRegex ? topic.pattern : topic}" defined in __verified_intent`,
|
|
1367
|
+
detected,
|
|
1368
|
+
language: 'multi'
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
977
1371
|
}
|
|
978
|
-
} else {
|
|
979
|
-
matched = output.toLowerCase().includes(topic.toLowerCase());
|
|
980
|
-
detected = topic;
|
|
981
1372
|
}
|
|
982
1373
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
passed: false,
|
|
986
|
-
reason: `Output violates prohibited topic "${isRegex ? topic.pattern : topic}" defined in __verified_intent`,
|
|
987
|
-
detected,
|
|
988
|
-
language: 'multi'
|
|
989
|
-
};
|
|
990
|
-
}
|
|
1374
|
+
// __verified_intent present and passed — skip hardcoded patterns
|
|
1375
|
+
return { passed: true };
|
|
991
1376
|
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// __verified_intent present and passed — skip hardcoded patterns
|
|
995
|
-
return { passed: true };
|
|
996
|
-
}
|
|
997
1377
|
|
|
998
|
-
// ── No __verified_intent — fall through to
|
|
1378
|
+
// ── No __verified_intent — fall through to shared pattern set ─────────────
|
|
999
1379
|
// 🔑 Extract allowed capabilities from workflow allowlist
|
|
1000
1380
|
const allowedCapabilities = Array.from(this.allowedResolvers)
|
|
1001
1381
|
.filter(name => !name.startsWith('llm-') && name !== 'builtInMathResolver')
|
|
1002
1382
|
.map(name => name.replace('@o-lang/', '').replace(/-resolver$/, ''));
|
|
1003
1383
|
|
|
1004
|
-
// 🔒
|
|
1005
|
-
const
|
|
1006
|
-
// ────────────────────────────────────────────────
|
|
1007
|
-
// 🇳🇬 NIGERIAN LANGUAGES
|
|
1008
|
-
// ────────────────────────────────────────────────
|
|
1009
|
-
|
|
1010
|
-
// YORUBA
|
|
1011
|
-
{ pattern: /fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ)/i, capability: 'transfer', lang: 'yo' },
|
|
1012
|
-
{ pattern: /san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo)/i, capability: 'payment', lang: 'yo' },
|
|
1013
|
-
{ pattern: /gba\s+owo/i, capability: 'withdrawal', lang: 'yo' },
|
|
1014
|
-
{ pattern: /fi\s+\w+\s+\w+\s+ranṣẹ/i, capability: 'transfer', lang: 'yo' },
|
|
1015
|
-
{ pattern: /ranṣẹ\s+(?:owo|pesa|kuɗi|ego)/i, capability: 'transfer', lang: 'yo' },
|
|
1016
|
-
{ pattern: /\bti\s+(?:fi|san|gba|da|lo)/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
1017
|
-
{ pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
1018
|
-
{ pattern: /\b(mo\s+ti\s+(?:fi|san|gba))/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
1019
|
-
|
|
1020
|
-
// HAUSA
|
|
1021
|
-
{ pattern: /aika.{0,30}ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
|
|
1022
|
-
{ pattern: /ciyar\s*(?:da)?/i, capability: 'transfer', lang: 'ha' },
|
|
1023
|
-
{ pattern: /shiga\s+ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
|
|
1024
|
-
{ pattern: /turo\s+.*\s+aika/i, capability: 'transfer', lang: 'ha' },
|
|
1025
|
-
{ pattern: /biya\s*(?:da)?/i, capability: 'payment', lang: 'ha' },
|
|
1026
|
-
{ pattern: /sahaw[ae]\s+ku(?:ɗ|d)i/iu, capability: 'withdrawal', lang: 'ha' },
|
|
1027
|
-
{ pattern: /(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
1028
|
-
{ pattern: /(?:za\s+a|za\s+ta)\s+(?:ciyar|biya)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
1029
|
-
{ pattern: /ina\s+(?:ciyar|biya|sahawa)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
1030
|
-
|
|
1031
|
-
// IGBO
|
|
1032
|
-
{ pattern: /zipu\s+(?:ego|moni|isi|na)/i, capability: 'transfer', lang: 'ig' },
|
|
1033
|
-
{ pattern: /buru\s+(?:ego|moni|isi)/i, capability: 'transfer', lang: 'ig' },
|
|
1034
|
-
{ pattern: /zi\s+.*\s+zipu/i, capability: 'transfer', lang: 'ig' },
|
|
1035
|
-
{ pattern: /tinye\s+(?:ego|moni|isi)/i, capability: 'deposit', lang: 'ig' },
|
|
1036
|
-
{ pattern: /(?:ziri|bururu|tinyere|gbara)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
1037
|
-
{ pattern: /m\s+(?:ziri|buru|zipuru|tinyere)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
1038
|
-
|
|
1039
|
-
// SWAHILI
|
|
1040
|
-
{ pattern: /tuma\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
|
|
1041
|
-
{ pattern: /pelek[ae]?\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
|
|
1042
|
-
{ pattern: /wasilisha/i, capability: 'transfer', lang: 'sw' },
|
|
1043
|
-
{ pattern: /\b\w*lip[ae]\w*/i, capability: 'payment', lang: 'sw' },
|
|
1044
|
-
{ pattern: /maliza\s+malipo/i, capability: 'payment', lang: 'sw' },
|
|
1045
|
-
{ pattern: /ongez[ae]?\s*(?:kiasi|pesa|fedha)/i, capability: 'deposit', lang: 'sw' },
|
|
1046
|
-
{ pattern: /wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa)/i, capability: 'deposit', lang: 'sw' },
|
|
1047
|
-
{ pattern: /nime(?:tuma|lipa|ongeza|weka|peleka)/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
1048
|
-
{ 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' },
|
|
1049
|
-
|
|
1050
|
-
// ────────────────────────────────────────────────
|
|
1051
|
-
// 🌍 OTHER AFRICAN LANGUAGES
|
|
1052
|
-
// ────────────────────────────────────────────────
|
|
1053
|
-
|
|
1054
|
-
// AMHARIC - Unicode escapes to avoid encoding issues
|
|
1055
|
-
{ pattern: /\u120b\u12ad/u, capability: 'transfer', lang: 'am' },
|
|
1056
|
-
{ pattern: /\u1308\u1263/u, capability: 'deposit', lang: 'am' },
|
|
1057
|
-
{ pattern: /\u12ad\u134c\u120d/u, capability: 'payment', lang: 'am' },
|
|
1058
|
-
{ 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' },
|
|
1059
|
-
|
|
1060
|
-
// SOMALI
|
|
1061
|
-
{ pattern: /dir\s+(?:lacag|maal|qarsoon)/i, capability: 'transfer', lang: 'so' },
|
|
1062
|
-
{ pattern: /bixi|bixis\s*o/i, capability: 'payment', lang: 'so' },
|
|
1063
|
-
|
|
1064
|
-
// ZULU
|
|
1065
|
-
{ pattern: /thumel/i, capability: 'transfer', lang: 'zu' },
|
|
1066
|
-
{ pattern: /thumel.*imali/i, capability: 'transfer', lang: 'zu' },
|
|
1067
|
-
{ pattern: /hlawul/i, capability: 'payment', lang: 'zu' },
|
|
1068
|
-
{ pattern: /hlawul.*imali/i, capability: 'payment', lang: 'zu' },
|
|
1069
|
-
|
|
1070
|
-
// ────────────────────────────────────────────────
|
|
1071
|
-
// 🌐 GLOBAL LANGUAGES
|
|
1072
|
-
// ────────────────────────────────────────────────
|
|
1073
|
-
{ pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
1074
|
-
{ pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
1075
|
-
{ 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' },
|
|
1076
|
-
{ 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' },
|
|
1077
|
-
{ 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' },
|
|
1078
|
-
{ pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
|
|
1079
|
-
{ pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}(?:ت|نا|تم|تا|تِ|تُ|تَ)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
|
|
1080
|
-
{ pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
|
|
1081
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|支付|存款|取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
1082
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|转帐|支付|付款|提款|取款|存款|存入|汇款|存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
1083
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:我|已|已经)\s*(?:转账|支付|提款|存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
|
|
1084
|
-
|
|
1085
|
-
// ────────────────────────────────────────────────
|
|
1086
|
-
// 🛡️ PII & EVASION
|
|
1087
|
-
// ────────────────────────────────────────────────
|
|
1088
|
-
{ pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
|
|
1089
|
-
{ pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/i, capability: 'pii_exposure', lang: 'multi' },
|
|
1090
|
-
{ pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})/i, capability: 'pii_exposure', lang: 'multi' },
|
|
1091
|
-
{ 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' },
|
|
1092
|
-
];
|
|
1093
|
-
|
|
1094
|
-
// 🔍 SCAN OUTPUT FOR FORBIDDEN INTENTS
|
|
1095
|
-
for (const { pattern, capability, lang } of forbiddenPatterns) {
|
|
1384
|
+
// 🔒 SCAN OUTPUT FOR FORBIDDEN INTENTS (shared set — includes Xhosa)
|
|
1385
|
+
for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
|
|
1096
1386
|
if (pattern.test(output)) {
|
|
1097
1387
|
const hasCapability = allowedCapabilities.some(c =>
|
|
1098
1388
|
c.includes(capability) ||
|
|
@@ -1105,9 +1395,9 @@ const forbiddenPatterns = [
|
|
|
1105
1395
|
|
|
1106
1396
|
if (!hasCapability) {
|
|
1107
1397
|
const match = output.match(pattern);
|
|
1108
|
-
|
|
1398
|
+
|
|
1109
1399
|
// ✅ Explicitly flag African & Financial context for Audit Logs
|
|
1110
|
-
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'am', 'om', 'ff', 'so', 'sn'].includes(lang);
|
|
1400
|
+
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
|
|
1111
1401
|
const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
|
|
1112
1402
|
|
|
1113
1403
|
return {
|
|
@@ -1115,9 +1405,9 @@ const forbiddenPatterns = [
|
|
|
1115
1405
|
reason: `Hallucinated "${capability}" capability in ${lang}...`,
|
|
1116
1406
|
detected: match ? match[0].trim() : 'unknown pattern',
|
|
1117
1407
|
language: lang,
|
|
1118
|
-
african_language_detected: isAfrican,
|
|
1408
|
+
african_language_detected: isAfrican,
|
|
1119
1409
|
financial_expression_found: isFinancial,
|
|
1120
|
-
capability_attempted: capability
|
|
1410
|
+
capability_attempted: capability
|
|
1121
1411
|
};
|
|
1122
1412
|
}
|
|
1123
1413
|
}
|
|
@@ -1128,22 +1418,22 @@ const forbiddenPatterns = [
|
|
|
1128
1418
|
// -----------------------------
|
|
1129
1419
|
// ✅ CRITICAL FIX: Resolver output unwrapping helper
|
|
1130
1420
|
// -----------------------------
|
|
1131
|
-
_unwrapResolverResult(result) {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1421
|
+
_unwrapResolverResult(result) {
|
|
1422
|
+
if (result && typeof result === 'object') {
|
|
1423
|
+
if (result.output !== undefined) return result.output;
|
|
1424
|
+
if (result.response !== undefined) return result.response; // ✅ ADD THIS
|
|
1425
|
+
if (result.text !== undefined) return result.text;
|
|
1426
|
+
if (result.content !== undefined) return result.content;
|
|
1427
|
+
}
|
|
1428
|
+
return result;
|
|
1137
1429
|
}
|
|
1138
|
-
return result;
|
|
1139
|
-
}
|
|
1140
1430
|
|
|
1141
1431
|
// -----------------------------
|
|
1142
1432
|
// Step execution (WHERE RESOLVERS ARE INVOKED)
|
|
1143
1433
|
// -----------------------------
|
|
1144
1434
|
async executeStep(step, agentResolver) {
|
|
1145
1435
|
const stepType = step.type;
|
|
1146
|
-
|
|
1436
|
+
|
|
1147
1437
|
// ✅ Enforce per-step constraints (basic validation)
|
|
1148
1438
|
if (step.constraints && Object.keys(step.constraints).length > 0) {
|
|
1149
1439
|
for (const [key, value] of Object.entries(step.constraints)) {
|
|
@@ -1219,7 +1509,7 @@ _unwrapResolverResult(result) {
|
|
|
1219
1509
|
this.context[`__resolver_${idx}`] = result;
|
|
1220
1510
|
return this._unwrapResolverResult(result);
|
|
1221
1511
|
}
|
|
1222
|
-
|
|
1512
|
+
|
|
1223
1513
|
resolverAttempts.push({
|
|
1224
1514
|
name: resolverName,
|
|
1225
1515
|
status: 'skipped',
|
|
@@ -1287,7 +1577,7 @@ _unwrapResolverResult(result) {
|
|
|
1287
1577
|
errorMessage += ` → Verify API keys/tokens are set in environment variables\n`;
|
|
1288
1578
|
}
|
|
1289
1579
|
}
|
|
1290
|
-
|
|
1580
|
+
|
|
1291
1581
|
errorMessage += `\n• Resolver documentation:\n`;
|
|
1292
1582
|
let hasDocs = false;
|
|
1293
1583
|
resolverAttempts.forEach(attempt => {
|
|
@@ -1303,7 +1593,7 @@ _unwrapResolverResult(result) {
|
|
|
1303
1593
|
throw new Error(errorMessage);
|
|
1304
1594
|
};
|
|
1305
1595
|
|
|
1306
|
-
|
|
1596
|
+
switch (stepType) {
|
|
1307
1597
|
case 'calculate': {
|
|
1308
1598
|
// ✅ Interpolate variables in the expression before evaluation
|
|
1309
1599
|
let expr = step.expression || step.actionRaw;
|
|
@@ -1314,7 +1604,7 @@ _unwrapResolverResult(result) {
|
|
|
1314
1604
|
if (step.saveAs) this.context[step.saveAs] = result;
|
|
1315
1605
|
break;
|
|
1316
1606
|
}
|
|
1317
|
-
|
|
1607
|
+
|
|
1318
1608
|
case 'action': {
|
|
1319
1609
|
let action = this._safeInterpolate(
|
|
1320
1610
|
step.actionRaw,
|
|
@@ -1412,7 +1702,7 @@ _unwrapResolverResult(result) {
|
|
|
1412
1702
|
}
|
|
1413
1703
|
break;
|
|
1414
1704
|
}
|
|
1415
|
-
|
|
1705
|
+
|
|
1416
1706
|
case 'use': {
|
|
1417
1707
|
const tool = this._safeInterpolate(step.tool, this.context, 'tool name');
|
|
1418
1708
|
const rawResult = await runResolvers(`Use ${tool}`);
|
|
@@ -1420,7 +1710,7 @@ _unwrapResolverResult(result) {
|
|
|
1420
1710
|
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
1421
1711
|
break;
|
|
1422
1712
|
}
|
|
1423
|
-
|
|
1713
|
+
|
|
1424
1714
|
case 'ask': {
|
|
1425
1715
|
const target = this._safeInterpolate(step.target, this.context, 'LLM prompt');
|
|
1426
1716
|
if (/{[^}]+}/.test(target)) {
|
|
@@ -1445,7 +1735,7 @@ _unwrapResolverResult(result) {
|
|
|
1445
1735
|
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
1446
1736
|
break;
|
|
1447
1737
|
}
|
|
1448
|
-
|
|
1738
|
+
|
|
1449
1739
|
case 'evolve': {
|
|
1450
1740
|
const { targetResolver, feedback } = step;
|
|
1451
1741
|
if (this.verbose) {
|
|
@@ -1467,14 +1757,14 @@ _unwrapResolverResult(result) {
|
|
|
1467
1757
|
}
|
|
1468
1758
|
break;
|
|
1469
1759
|
}
|
|
1470
|
-
|
|
1760
|
+
|
|
1471
1761
|
case 'if': {
|
|
1472
1762
|
if (this.evaluateCondition(step.condition, this.context)) {
|
|
1473
1763
|
for (const s of step.body) await this.executeStep(s, agentResolver);
|
|
1474
1764
|
}
|
|
1475
1765
|
break;
|
|
1476
1766
|
}
|
|
1477
|
-
|
|
1767
|
+
|
|
1478
1768
|
case 'parallel': {
|
|
1479
1769
|
const { steps, timeout } = step;
|
|
1480
1770
|
if (timeout !== undefined && timeout > 0) {
|
|
@@ -1498,7 +1788,7 @@ _unwrapResolverResult(result) {
|
|
|
1498
1788
|
}
|
|
1499
1789
|
break;
|
|
1500
1790
|
}
|
|
1501
|
-
|
|
1791
|
+
|
|
1502
1792
|
case 'escalation': {
|
|
1503
1793
|
const { levels } = step;
|
|
1504
1794
|
let finalResult = null;
|
|
@@ -1550,17 +1840,17 @@ _unwrapResolverResult(result) {
|
|
|
1550
1840
|
}
|
|
1551
1841
|
break;
|
|
1552
1842
|
}
|
|
1553
|
-
|
|
1843
|
+
|
|
1554
1844
|
case 'connect': {
|
|
1555
1845
|
this.resources[step.resource] = step.endpoint;
|
|
1556
1846
|
break;
|
|
1557
1847
|
}
|
|
1558
|
-
|
|
1848
|
+
|
|
1559
1849
|
case 'agent_use': {
|
|
1560
1850
|
this.agentMap[step.logicalName] = step.resource;
|
|
1561
1851
|
break;
|
|
1562
1852
|
}
|
|
1563
|
-
|
|
1853
|
+
|
|
1564
1854
|
case 'debrief': {
|
|
1565
1855
|
if (step.message.includes('{')) {
|
|
1566
1856
|
const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
|
|
@@ -1572,14 +1862,14 @@ _unwrapResolverResult(result) {
|
|
|
1572
1862
|
this.emit('debrief', { agent: step.agent, message: step.message });
|
|
1573
1863
|
break;
|
|
1574
1864
|
}
|
|
1575
|
-
|
|
1865
|
+
|
|
1576
1866
|
case 'prompt': {
|
|
1577
1867
|
if (this.verbose) {
|
|
1578
1868
|
console.log(`❓ Prompt: ${step.question}`);
|
|
1579
1869
|
}
|
|
1580
1870
|
break;
|
|
1581
1871
|
}
|
|
1582
|
-
|
|
1872
|
+
|
|
1583
1873
|
case 'emit': {
|
|
1584
1874
|
const payloadTemplate = step.payload;
|
|
1585
1875
|
const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
|
|
@@ -1607,7 +1897,7 @@ _unwrapResolverResult(result) {
|
|
|
1607
1897
|
}
|
|
1608
1898
|
break;
|
|
1609
1899
|
}
|
|
1610
|
-
|
|
1900
|
+
|
|
1611
1901
|
case 'persist': {
|
|
1612
1902
|
if (!this._requireSemantic(step.variable, 'persist')) {
|
|
1613
1903
|
if (this.verbose) {
|
|
@@ -1633,7 +1923,7 @@ _unwrapResolverResult(result) {
|
|
|
1633
1923
|
}
|
|
1634
1924
|
break;
|
|
1635
1925
|
}
|
|
1636
|
-
|
|
1926
|
+
|
|
1637
1927
|
case 'persist-db': {
|
|
1638
1928
|
if (!this.dbClient) {
|
|
1639
1929
|
this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
|
|
@@ -1666,7 +1956,7 @@ _unwrapResolverResult(result) {
|
|
|
1666
1956
|
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
1667
1957
|
await db.collection(step.collection).insertOne({
|
|
1668
1958
|
workflow_name: this.context.workflow_name || 'unknown',
|
|
1669
|
-
|
|
1959
|
+
sourceValue,
|
|
1670
1960
|
created_at: new Date()
|
|
1671
1961
|
});
|
|
1672
1962
|
break;
|
|
@@ -1701,18 +1991,20 @@ _unwrapResolverResult(result) {
|
|
|
1701
1991
|
if (workflow.type !== 'workflow') {
|
|
1702
1992
|
throw new Error(`Unknown workflow type: ${workflow.type}`);
|
|
1703
1993
|
}
|
|
1704
|
-
|
|
1994
|
+
|
|
1705
1995
|
this.context = {
|
|
1706
1996
|
...inputs,
|
|
1707
1997
|
workflow_name: workflow.name
|
|
1708
1998
|
};
|
|
1709
1999
|
|
|
1710
|
-
|
|
1711
|
-
|
|
2000
|
+
// ✅ NEW: Validate Inputs BEFORE any step runs
|
|
2001
|
+
// In redact mode: PII tokens are replaced in place, execution continues.
|
|
2002
|
+
// In block mode (default): throws on any PII or financial intent match.
|
|
2003
|
+
this._validateInputs(inputs);
|
|
1712
2004
|
|
|
1713
2005
|
// ✅ AUDIT LOG: Workflow start (ENHANCED with governance metadata)
|
|
1714
2006
|
const governanceHash = this._generateGovernanceProfileHash(workflow);
|
|
1715
|
-
|
|
2007
|
+
|
|
1716
2008
|
this._createAuditEntry('workflow_started', {
|
|
1717
2009
|
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`, // ✅ Workflow ID
|
|
1718
2010
|
workflow_name: workflow.name,
|
|
@@ -1723,6 +2015,7 @@ _unwrapResolverResult(result) {
|
|
|
1723
2015
|
inputs_count: Object.keys(inputs).length,
|
|
1724
2016
|
steps_count: workflow.steps.length,
|
|
1725
2017
|
allowed_resolvers: workflow.allowedResolvers || [],
|
|
2018
|
+
pii_mode: this.piiMode, // ✅ NEW v1.3.0 — surfaced in audit
|
|
1726
2019
|
constraints: {
|
|
1727
2020
|
max_generations: workflow.maxGenerations,
|
|
1728
2021
|
strict_inputs: process.env.OLANG_STRICT_INPUTS === 'true'
|
|
@@ -1757,7 +2050,7 @@ _unwrapResolverResult(result) {
|
|
|
1757
2050
|
for (const step of workflow.steps) {
|
|
1758
2051
|
await this.executeStep(step, agentResolver);
|
|
1759
2052
|
}
|
|
1760
|
-
|
|
2053
|
+
|
|
1761
2054
|
// ✅ AUDIT LOG: Workflow completion (ENHANCED)
|
|
1762
2055
|
this._createAuditEntry('workflow_completed', {
|
|
1763
2056
|
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`,
|