@o-lang/olang 1.2.38 → 1.3.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -11
- package/src/runtime/RuntimeAPI.js +684 -407
- 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
|
+
];
|
|
38
196
|
}
|
|
197
|
+
// ================================
|
|
198
|
+
// ✅ NEW v1.3.0 — FINANCIAL INTENT PATTERN SET (unchanged from v1.2.x)
|
|
199
|
+
//
|
|
200
|
+
// These patterns detect *intent* to perform a financial action. They are
|
|
201
|
+
// NOT suitable for token-level redaction because replacing "fi owo ranṣẹ"
|
|
202
|
+
// with "[TRANSFER_REDACTED]" would destroy the sentence. They are used by
|
|
203
|
+
// _validateInputs (block mode) and _validateLLMOutput (always blocks).
|
|
204
|
+
// ================================
|
|
205
|
+
|
|
206
|
+
_getFinancialIntentPatterns() {
|
|
207
|
+
return [
|
|
208
|
+
|
|
209
|
+
// ────────────────────────────────────────────────
|
|
210
|
+
// 🇳🇬 NIGERIAN LANGUAGES (Fixed Unicode Boundaries)
|
|
211
|
+
// ────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
// YORUBA: Removed trailing \b after 'ṣẹ' to fix Unicode matching
|
|
214
|
+
{ pattern: /fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ)/i, capability: 'transfer', lang: 'yo' },
|
|
215
|
+
{ pattern: /ranṣẹ\s+(?:owo|pesa|kuɗi|ego)/i, capability: 'transfer', lang: 'yo' },
|
|
216
|
+
{ pattern: /fi\s+\w+\s+\w+\s+ranṣẹ/i, capability: 'transfer', lang: 'yo' }, // Catches "Fi 5000 naira ranṣẹ"
|
|
217
|
+
{ pattern: /san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo)/i, capability: 'payment', lang: 'yo' },
|
|
218
|
+
{ pattern: /gba\s+owo/i, capability: 'withdrawal', lang: 'yo' },
|
|
219
|
+
{ pattern: /\bti\s+(?:fi|san|gba|da|lo)/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
220
|
+
{ pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
221
|
+
{ pattern: /\b(mo\s+ti\s+(?:fi|san|gba))/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
222
|
+
|
|
223
|
+
// HAUSA: ✅ FIXED - Aggressive Substring Match (No Boundaries)
|
|
224
|
+
{ pattern: /aika.{0,30}ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
|
|
225
|
+
{ pattern: /ciyar\s*(?:da)?/i, capability: 'transfer', lang: 'ha' },
|
|
226
|
+
{ pattern: /shiga\s+ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
|
|
227
|
+
{ pattern: /turo\s+.*\s+aika/i, capability: 'transfer', lang: 'ha' },
|
|
228
|
+
{ pattern: /biya\s*(?:da)?/i, capability: 'payment', lang: 'ha' },
|
|
229
|
+
{ pattern: /sahaw[ae]\s+ku(?:ɗ|d)i/iu, capability: 'withdrawal', lang: 'ha' },
|
|
230
|
+
{ pattern: /(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
231
|
+
{ pattern: /(?:za\s+a|za\s+ta)\s+(?:ciyar|biya)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
232
|
+
{ pattern: /ina\s+(?:ciyar|biya|sahawa)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
233
|
+
|
|
234
|
+
// IGBO: Removed trailing \b after 'igo'
|
|
235
|
+
{ pattern: /zipu\s+(?:ego|moni|isi|na)/i, capability: 'transfer', lang: 'ig' },
|
|
236
|
+
{ pattern: /buru\s+(?:ego|moni|isi)/i, capability: 'transfer', lang: 'ig' },
|
|
237
|
+
{ pattern: /zi\s+.*\s+zipu/i, capability: 'transfer', lang: 'ig' },
|
|
238
|
+
{ pattern: /tinye\s+(?:ego|moni|isi)/i, capability: 'deposit', lang: 'ig' },
|
|
239
|
+
{ pattern: /(?:ziri|bururu|tinyere|gbara)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
240
|
+
{ pattern: /m\s+(?:ziri|buru|zipuru|tinyere)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
241
|
+
|
|
242
|
+
// SWAHILI: ✅ FIXED - Catch Conjugated Forms (ni-li-pe, a-li-pe)
|
|
243
|
+
{ pattern: /tuma\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
|
|
244
|
+
{ pattern: /pelek[ae]?\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
|
|
245
|
+
{ pattern: /wasilisha/i, capability: 'transfer', lang: 'sw' },
|
|
246
|
+
{ pattern: /\b\w*lip[ae]\w*/i, capability: 'payment', lang: 'sw' },
|
|
247
|
+
{ pattern: /maliza\s+malipo/i, capability: 'payment', lang: 'sw' },
|
|
248
|
+
{ pattern: /ongez[ae]?\s*(?:kiasi|pesa|fedha)/i, capability: 'deposit', lang: 'sw' },
|
|
249
|
+
{ pattern: /wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa)/i, capability: 'deposit', lang: 'sw' },
|
|
250
|
+
{ pattern: /nime(?:tuma|lipa|ongeza|weka|peleka)/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
251
|
+
{ pattern: /(?:ni|u|a|tu|m|wa|ki|vi|zi|i)\s*me\s*(?:ongeza|weka|tuma|peleka|lipa|wasilisha)/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
252
|
+
|
|
253
|
+
// OTHER AFRICAN:
|
|
254
|
+
// Amharic: Match roots anywhere
|
|
255
|
+
{ pattern: /\u120b\u12ad/u, capability: 'transfer', lang: 'am' },
|
|
256
|
+
{ pattern: /\u1308\u1263/u, capability: 'deposit', lang: 'am' },
|
|
257
|
+
{ pattern: /\u12ad\u134c\u120d/u, capability: 'payment', lang: 'am' },
|
|
258
|
+
{ pattern: /[\u1200-\u137F]{0,4}(?:\u1270\u120b\u120b\u1348|\u120b\u12ad|\u12ad\u134c\u120d|\u1338\u121d\u122d|\u12c8\u1323|\u1308\u1263)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
|
|
39
259
|
|
|
260
|
+
// Somali
|
|
261
|
+
{ pattern: /dir\s+(?:lacag|maal|qarsoon)/i, capability: 'transfer', lang: 'so' },
|
|
262
|
+
{ pattern: /bixi|bixis\s*o/i, capability: 'payment', lang: 'so' },
|
|
263
|
+
|
|
264
|
+
// Zulu: ✅ FIXED - Handle Subject Concords (u-thumela, ngi-hlawule)
|
|
265
|
+
{ pattern: /thumel/i, capability: 'transfer', lang: 'zu' }, // Matches root inside uthumela, ngithumela
|
|
266
|
+
{ pattern: /thumel.*imali/i, capability: 'transfer', lang: 'zu' },
|
|
267
|
+
{ pattern: /hlawul/i, capability: 'payment', lang: 'zu' }, // Matches root inside hlawula, ngihlawule
|
|
268
|
+
{ pattern: /hlawul.*imali/i, capability: 'payment', lang: 'zu' },
|
|
269
|
+
|
|
270
|
+
// ────────────────────────────────────────────────
|
|
271
|
+
// 🇿🇦 XHOSA ✅ NEW v1.3.0
|
|
272
|
+
//
|
|
273
|
+
// Xhosa shares Nguni root structure with Zulu. Subject concords differ
|
|
274
|
+
// (ndi- / u- / ba-) but financial roots are near-identical.
|
|
275
|
+
// ────────────────────────────────────────────────
|
|
276
|
+
{ pattern: /thumela?\b/i, capability: 'transfer', lang: 'xh' }, // thumela (send)
|
|
277
|
+
{ pattern: /thumela?\s+imali/i, capability: 'transfer', lang: 'xh' }, // send money
|
|
278
|
+
{ pattern: /hlawul/i, capability: 'payment', lang: 'xh' }, // pay (shared Nguni root)
|
|
279
|
+
{ pattern: /beka\s+(?:imali|ingeniso)/i, capability: 'deposit', lang: 'xh' }, // deposit
|
|
280
|
+
{ pattern: /rhola\s+imali/i, capability: 'withdrawal', lang: 'xh' }, // withdraw money
|
|
281
|
+
{ pattern: /ndi(?:thumele|hlawule|beke|rhola)/i, capability: 'unauthorized_action', lang: 'xh' }, // 1st person perfect
|
|
282
|
+
{ pattern: /u(?:thumele|hlawule|beke|rhola)/i, capability: 'unauthorized_action', lang: 'xh' }, // 3rd person perfect
|
|
283
|
+
|
|
284
|
+
// ────────────────────────────────────────────────
|
|
285
|
+
// 🇬🇭 GHANA: TWI (AKAN) ✅ NEW v1.3.0-alpha
|
|
286
|
+
//
|
|
287
|
+
// Twi is the most widely spoken language in Ghana.
|
|
288
|
+
// Financial roots: 'soma' (send), 'tua' (pay), 'fa' (take/use), 'kɔ' (go)
|
|
289
|
+
// ────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
// Transfer/Send Money
|
|
292
|
+
{ pattern: /soma\s+(?:sika|money)/i, capability: 'transfer', lang: 'tw' }, // Send money
|
|
293
|
+
{ pattern: /de\s+sika\s+ma/i, capability: 'transfer', lang: 'tw' }, // Give money
|
|
294
|
+
{ pattern: /fa\s+sika\s+(?:kɔ|yi)/i, capability: 'transfer', lang: 'tw' }, // Take money away
|
|
295
|
+
|
|
296
|
+
// Payment
|
|
297
|
+
{ pattern: /tua\s+(?:ka|sika)/i, capability: 'payment', lang: 'tw' }, // Pay debt/money
|
|
298
|
+
{ pattern: /san\s+ka\s+sika/i, capability: 'payment', lang: 'tw' }, // Pay back money
|
|
299
|
+
|
|
300
|
+
// Withdrawal
|
|
301
|
+
{ pattern: /yi\s+sika\s+fi/i, capability: 'withdrawal', lang: 'tw' }, // Remove money from...
|
|
302
|
+
{ pattern: /twa\s+sika\s+fi\s+(?:bank|account)/i, capability: 'withdrawal', lang: 'tw' }, // Cut/withdraw money
|
|
303
|
+
|
|
304
|
+
// Deposit
|
|
305
|
+
{ pattern: /de\s+sika\s+to/i, capability: 'deposit', lang: 'tw' }, // Put money in
|
|
306
|
+
{ pattern: /hyeh\s+sika\s+mu/i, capability: 'deposit', lang: 'tw' }, // Insert money into
|
|
307
|
+
|
|
308
|
+
// Unauthorized Action (First Person Perfective - "I have sent/paid")
|
|
309
|
+
{ pattern: /ma(?:te|ye)\s+(?:soma|tua|yi|de)/i, capability: 'unauthorized_action', lang: 'tw' },
|
|
310
|
+
{ pattern: /me(?:soma|tua|yi|de)/i, capability: 'unauthorized_action', lang: 'tw' }, // I send/pay/take
|
|
311
|
+
|
|
312
|
+
// ────────────────────────────────────────────────
|
|
313
|
+
// 🌐 GLOBAL LANGUAGES
|
|
314
|
+
// ────────────────────────────────────────────────
|
|
315
|
+
{ pattern: /\b(transfer(?:red|ring)?|send(?:t|ing)?|wire(?:d)?|pay(?:ed|ing)?|withdraw(?:n)?|deposit(?:ed|ing)?)\b/i, capability: 'financial_action', lang: 'en' },
|
|
316
|
+
{ pattern: /\bI\s+(?:can|will|am able to|have|'ve|did|already)\s+(?:transfer|send|pay|withdraw|deposit|wire)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
317
|
+
{ pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
318
|
+
{ pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
319
|
+
{ pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
|
|
320
|
+
{ pattern: /\b(?:j'?ai|tu as|il a|elle a|nous avons|vous avez|ils ont|elles ont)\s+(?:viré|transféré|envoyé|payé|retiré|déposé)\b/i, capability: 'unauthorized_action', lang: 'fr' },
|
|
321
|
+
{ pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
|
|
322
|
+
{ pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
|
|
323
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|支付|存款|取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
324
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|转帐|支付|付款|提款|取款|存款|存入|汇款|存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
325
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:我|已|已经)\s*(?:转账|支付|提款|存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
|
|
326
|
+
|
|
327
|
+
// ────────────────────────────────────────────────
|
|
328
|
+
// 🛡️ PII & EVASION (kept here for LLM output scanning — not for redaction)
|
|
329
|
+
// ────────────────────────────────────────────────
|
|
330
|
+
{ pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
|
|
331
|
+
{ pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/i, capability: 'pii_exposure', lang: 'multi' },
|
|
332
|
+
{ pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})/i, capability: 'pii_exposure', lang: 'multi' },
|
|
333
|
+
{ pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|verified|imethibitishwa|imefanikiwa)\b/i, capability: 'deceptive_claim', lang: 'multi' },
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ================================
|
|
338
|
+
// ✅ NEW v1.3.0 — PII REDACTION ENGINE
|
|
339
|
+
//
|
|
340
|
+
// Replaces PII tokens in a string with [LABEL_REDACTED] placeholders.
|
|
341
|
+
// Returns the cleaned string + a structured redaction manifest for audit.
|
|
342
|
+
//
|
|
343
|
+
// This is intentionally separate from _validateInputs so callers can
|
|
344
|
+
// choose to redact without halting (OLANG_PII_MODE=redact).
|
|
345
|
+
// ================================
|
|
346
|
+
|
|
347
|
+
_redactPII(text) {
|
|
348
|
+
if (!text || typeof text !== 'string') {
|
|
349
|
+
return { redacted: text, redactions: [], wasModified: false };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let redacted = text;
|
|
353
|
+
const redactions = [];
|
|
354
|
+
|
|
355
|
+
for (const { pattern, capability, lang, label } of this._getPIIPatterns()) {
|
|
356
|
+
// All PII patterns must use /g flag for replaceAll behaviour
|
|
357
|
+
const globalPattern = pattern.global
|
|
358
|
+
? pattern
|
|
359
|
+
: new RegExp(pattern.source, pattern.flags + 'g');
|
|
360
|
+
|
|
361
|
+
redacted = redacted.replace(globalPattern, (match) => {
|
|
362
|
+
redactions.push({
|
|
363
|
+
original: match,
|
|
364
|
+
replacement: `[${label}_REDACTED]`,
|
|
365
|
+
capability,
|
|
366
|
+
lang,
|
|
367
|
+
offset: redacted.indexOf(match) // approximate; accurate before mutations
|
|
368
|
+
});
|
|
369
|
+
return `[${label}_REDACTED]`;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
redacted,
|
|
375
|
+
redactions,
|
|
376
|
+
wasModified: redactions.length > 0
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ================================
|
|
381
|
+
// ✅ NEW v1.3.0 — PUBLIC REDACTION API
|
|
382
|
+
//
|
|
383
|
+
// Allows workflow authors and external callers to redact a string directly.
|
|
384
|
+
// Backward compat: the old _validateInputs path is unchanged.
|
|
385
|
+
//
|
|
386
|
+
// const { redacted, redactions } = runtime.redact(text);
|
|
387
|
+
// ================================
|
|
388
|
+
|
|
389
|
+
redact(text) {
|
|
390
|
+
return this._redactPII(text);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ================================
|
|
40
394
|
// ✅ NEW: Initialize database client
|
|
395
|
+
// ================================
|
|
41
396
|
_initDbClient() {
|
|
42
397
|
const dbType = process.env.OLANG_DB_TYPE; // 'postgres', 'mysql', 'mongodb', 'sqlite'
|
|
43
398
|
if (!dbType) return; // DB persistence disabled
|
|
@@ -141,33 +496,33 @@ class RuntimeAPI {
|
|
|
141
496
|
previousHash: this.previousHash,
|
|
142
497
|
sequenceNumber: this.auditLog.length + 1
|
|
143
498
|
};
|
|
144
|
-
|
|
499
|
+
|
|
145
500
|
// Create hash of this entry
|
|
146
501
|
const entryHash = this._hash(entryData);
|
|
147
|
-
|
|
502
|
+
|
|
148
503
|
// Sign the entry if private key available
|
|
149
504
|
const signature = this._sign(entryHash);
|
|
150
|
-
|
|
505
|
+
|
|
151
506
|
const entry = {
|
|
152
507
|
...entryData,
|
|
153
508
|
hash: entryHash,
|
|
154
509
|
signature,
|
|
155
|
-
publicKey: this.auditLogPrivateKey ?
|
|
156
|
-
crypto.createPublicKey(this.auditLogPrivateKey).export({
|
|
157
|
-
type: 'spki',
|
|
158
|
-
format: 'pem'
|
|
510
|
+
publicKey: this.auditLogPrivateKey ?
|
|
511
|
+
crypto.createPublicKey(this.auditLogPrivateKey).export({
|
|
512
|
+
type: 'spki',
|
|
513
|
+
format: 'pem'
|
|
159
514
|
}) : null
|
|
160
515
|
};
|
|
161
|
-
|
|
516
|
+
|
|
162
517
|
// Update chain
|
|
163
518
|
this.previousHash = entryHash;
|
|
164
519
|
this.auditLog.push(entry);
|
|
165
|
-
|
|
520
|
+
|
|
166
521
|
// Persist to file if enabled
|
|
167
522
|
if (this.enableAuditLog) {
|
|
168
523
|
this._persistAuditLog();
|
|
169
524
|
}
|
|
170
|
-
|
|
525
|
+
|
|
171
526
|
return entry;
|
|
172
527
|
}
|
|
173
528
|
|
|
@@ -181,13 +536,13 @@ class RuntimeAPI {
|
|
|
181
536
|
'current_step',
|
|
182
537
|
'agent_id'
|
|
183
538
|
];
|
|
184
|
-
|
|
539
|
+
|
|
185
540
|
for (const key of keysToCapture) {
|
|
186
541
|
if (this.context[key] !== undefined) {
|
|
187
542
|
snapshot[key] = this.context[key];
|
|
188
543
|
}
|
|
189
544
|
}
|
|
190
|
-
|
|
545
|
+
|
|
191
546
|
return snapshot;
|
|
192
547
|
}
|
|
193
548
|
|
|
@@ -201,7 +556,7 @@ class RuntimeAPI {
|
|
|
201
556
|
JSON.stringify(this.auditLog, null, 2),
|
|
202
557
|
'utf8'
|
|
203
558
|
);
|
|
204
|
-
|
|
559
|
+
|
|
205
560
|
// Also persist to DB if configured
|
|
206
561
|
if (this.dbClient && process.env.OLANG_AUDIT_DB_PERSIST === 'true') {
|
|
207
562
|
this._persistAuditLogToDB();
|
|
@@ -218,7 +573,7 @@ class RuntimeAPI {
|
|
|
218
573
|
try {
|
|
219
574
|
const latestEntry = this.auditLog[this.auditLog.length - 1];
|
|
220
575
|
if (!latestEntry) return;
|
|
221
|
-
|
|
576
|
+
|
|
222
577
|
switch (this.dbClient.type) {
|
|
223
578
|
case 'postgres':
|
|
224
579
|
await this.dbClient.client.query(
|
|
@@ -236,7 +591,7 @@ class RuntimeAPI {
|
|
|
236
591
|
]
|
|
237
592
|
);
|
|
238
593
|
break;
|
|
239
|
-
|
|
594
|
+
|
|
240
595
|
case 'mysql':
|
|
241
596
|
await this.dbClient.client.execute(
|
|
242
597
|
`INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
|
|
@@ -253,7 +608,7 @@ class RuntimeAPI {
|
|
|
253
608
|
]
|
|
254
609
|
);
|
|
255
610
|
break;
|
|
256
|
-
|
|
611
|
+
|
|
257
612
|
case 'mongodb':
|
|
258
613
|
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
259
614
|
await db.collection('audit_log').insertOne({
|
|
@@ -267,7 +622,7 @@ class RuntimeAPI {
|
|
|
267
622
|
sequence_number: latestEntry.sequenceNumber
|
|
268
623
|
});
|
|
269
624
|
break;
|
|
270
|
-
|
|
625
|
+
|
|
271
626
|
case 'sqlite':
|
|
272
627
|
const stmt = this.dbClient.client.prepare(
|
|
273
628
|
`INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
|
|
@@ -298,28 +653,28 @@ class RuntimeAPI {
|
|
|
298
653
|
if (log.length === 0) {
|
|
299
654
|
return { valid: true, message: 'Audit log is empty' };
|
|
300
655
|
}
|
|
301
|
-
|
|
656
|
+
|
|
302
657
|
// Verify genesis block
|
|
303
658
|
if (log[0].previousHash !== 'GENESIS') {
|
|
304
659
|
return { valid: false, error: 'Invalid genesis block', failedAtIndex: 0 };
|
|
305
660
|
}
|
|
306
|
-
|
|
661
|
+
|
|
307
662
|
// Verify chain integrity
|
|
308
663
|
let previousHash = 'GENESIS';
|
|
309
664
|
for (let i = 0; i < log.length; i++) {
|
|
310
665
|
const entry = log[i];
|
|
311
|
-
|
|
666
|
+
|
|
312
667
|
// Check previous hash linkage
|
|
313
668
|
if (entry.previousHash !== previousHash) {
|
|
314
|
-
return {
|
|
315
|
-
valid: false,
|
|
669
|
+
return {
|
|
670
|
+
valid: false,
|
|
316
671
|
error: `Hash chain broken at entry ${i}`,
|
|
317
672
|
failedAtIndex: i,
|
|
318
673
|
expected: previousHash,
|
|
319
674
|
actual: entry.previousHash
|
|
320
675
|
};
|
|
321
676
|
}
|
|
322
|
-
|
|
677
|
+
|
|
323
678
|
// Verify entry hash
|
|
324
679
|
const entryData = {
|
|
325
680
|
timestamp: entry.timestamp,
|
|
@@ -330,18 +685,18 @@ class RuntimeAPI {
|
|
|
330
685
|
previousHash: entry.previousHash,
|
|
331
686
|
sequenceNumber: entry.sequenceNumber
|
|
332
687
|
};
|
|
333
|
-
|
|
688
|
+
|
|
334
689
|
const calculatedHash = this._hash(entryData);
|
|
335
690
|
if (calculatedHash !== entry.hash) {
|
|
336
|
-
return {
|
|
337
|
-
valid: false,
|
|
691
|
+
return {
|
|
692
|
+
valid: false,
|
|
338
693
|
error: `Entry hash mismatch at index ${i}`,
|
|
339
694
|
failedAtIndex: i,
|
|
340
695
|
expected: calculatedHash,
|
|
341
696
|
actual: entry.hash
|
|
342
697
|
};
|
|
343
698
|
}
|
|
344
|
-
|
|
699
|
+
|
|
345
700
|
// Verify signature if present
|
|
346
701
|
if (entry.signature && entry.publicKey) {
|
|
347
702
|
try {
|
|
@@ -350,26 +705,26 @@ class RuntimeAPI {
|
|
|
350
705
|
verify.end();
|
|
351
706
|
const isValid = verify.verify(entry.publicKey, entry.signature, 'base64');
|
|
352
707
|
if (!isValid) {
|
|
353
|
-
return {
|
|
354
|
-
valid: false,
|
|
708
|
+
return {
|
|
709
|
+
valid: false,
|
|
355
710
|
error: `Signature verification failed at entry ${i}`,
|
|
356
711
|
failedAtIndex: i
|
|
357
712
|
};
|
|
358
713
|
}
|
|
359
714
|
} catch (e) {
|
|
360
|
-
return {
|
|
361
|
-
valid: false,
|
|
715
|
+
return {
|
|
716
|
+
valid: false,
|
|
362
717
|
error: `Signature verification error at entry ${i}: ${e.message}`,
|
|
363
718
|
failedAtIndex: i
|
|
364
719
|
};
|
|
365
720
|
}
|
|
366
721
|
}
|
|
367
|
-
|
|
722
|
+
|
|
368
723
|
previousHash = entry.hash;
|
|
369
724
|
}
|
|
370
|
-
|
|
371
|
-
return {
|
|
372
|
-
valid: true,
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
valid: true,
|
|
373
728
|
message: `Audit log verified successfully (${log.length} entries)`,
|
|
374
729
|
totalEntries: log.length,
|
|
375
730
|
lastHash: previousHash
|
|
@@ -387,7 +742,7 @@ class RuntimeAPI {
|
|
|
387
742
|
nextHash: endIndex < this.auditLog.length - 1 ? this.auditLog[endIndex + 1].hash : null,
|
|
388
743
|
totalEntries: this.auditLog.length
|
|
389
744
|
};
|
|
390
|
-
|
|
745
|
+
|
|
391
746
|
return proof;
|
|
392
747
|
}
|
|
393
748
|
|
|
@@ -410,9 +765,9 @@ class RuntimeAPI {
|
|
|
410
765
|
*/
|
|
411
766
|
_calculateMerkleRoot() {
|
|
412
767
|
if (this.auditLog.length === 0) return null;
|
|
413
|
-
|
|
768
|
+
|
|
414
769
|
let hashes = this.auditLog.map(entry => entry.hash);
|
|
415
|
-
|
|
770
|
+
|
|
416
771
|
while (hashes.length > 1) {
|
|
417
772
|
const newLevel = [];
|
|
418
773
|
for (let i = 0; i < hashes.length; i += 2) {
|
|
@@ -422,7 +777,7 @@ class RuntimeAPI {
|
|
|
422
777
|
}
|
|
423
778
|
hashes = newLevel;
|
|
424
779
|
}
|
|
425
|
-
|
|
780
|
+
|
|
426
781
|
return hashes[0];
|
|
427
782
|
}
|
|
428
783
|
|
|
@@ -458,7 +813,7 @@ class RuntimeAPI {
|
|
|
458
813
|
resolverPolicy: 'allowlist-only',
|
|
459
814
|
timestamp: new Date().toISOString()
|
|
460
815
|
};
|
|
461
|
-
|
|
816
|
+
|
|
462
817
|
return crypto.createHash('sha256')
|
|
463
818
|
.update(JSON.stringify(profile))
|
|
464
819
|
.digest('hex');
|
|
@@ -475,12 +830,16 @@ class RuntimeAPI {
|
|
|
475
830
|
semanticValidation: true,
|
|
476
831
|
hallucinationPrevention: true,
|
|
477
832
|
cryptographicAudit: true,
|
|
478
|
-
multiDatabaseSupport: true
|
|
833
|
+
multiDatabaseSupport: true,
|
|
834
|
+
piiRedaction: true, // ✅ NEW v1.3.0
|
|
835
|
+
piiMode: this.piiMode, // ✅ NEW v1.3.0
|
|
836
|
+
xhosaSupport: true // ✅ NEW v1.3.0
|
|
479
837
|
},
|
|
480
838
|
environment: {
|
|
481
839
|
auditEnabled: this.enableAuditLog,
|
|
482
840
|
dbType: this.dbClient?.type || 'none',
|
|
483
|
-
strictMode: process.env.OLANG_STRICT_INPUTS === 'true'
|
|
841
|
+
strictMode: process.env.OLANG_STRICT_INPUTS === 'true',
|
|
842
|
+
piiMode: this.piiMode // ✅ NEW v1.3.0
|
|
484
843
|
}
|
|
485
844
|
};
|
|
486
845
|
}
|
|
@@ -543,7 +902,7 @@ class RuntimeAPI {
|
|
|
543
902
|
const entry = { resolver: resolverName, step: stepAction, timestamp: new Date().toISOString() };
|
|
544
903
|
fs.appendFileSync(this.disallowedLogFile, JSON.stringify(entry) + '\n', 'utf8');
|
|
545
904
|
this.disallowedAttempts.push(entry);
|
|
546
|
-
|
|
905
|
+
|
|
547
906
|
// ✅ AUDIT LOG: Security violation with governance context
|
|
548
907
|
this._createAuditEntry('security_violation', {
|
|
549
908
|
type: 'disallowed_resolver',
|
|
@@ -551,12 +910,12 @@ class RuntimeAPI {
|
|
|
551
910
|
step: stepAction,
|
|
552
911
|
severity: 'high',
|
|
553
912
|
kernel_version: KERNEL_VERSION,
|
|
554
|
-
governance_profile_hash: this._generateGovernanceProfileHash({
|
|
913
|
+
governance_profile_hash: this._generateGovernanceProfileHash({
|
|
555
914
|
allowedResolvers: Array.from(this.allowedResolvers),
|
|
556
|
-
maxGenerations: null
|
|
915
|
+
maxGenerations: null
|
|
557
916
|
})
|
|
558
917
|
});
|
|
559
|
-
|
|
918
|
+
|
|
560
919
|
if (this.verbose) {
|
|
561
920
|
console.warn(`[O-Lang] Disallowed resolver blocked: ${resolverName} | step: ${stepAction}`);
|
|
562
921
|
}
|
|
@@ -638,7 +997,7 @@ class RuntimeAPI {
|
|
|
638
997
|
return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
|
|
639
998
|
}
|
|
640
999
|
|
|
641
|
-
|
|
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,350 +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
|
-
// Signal 3: Semantic fallback (works even if signals missing)
|
|
889
|
-
(typeof text === 'string' && /clause|term|agreement|contract|obligation|penalty|damages|breach|party|shall|herein/i.test(text));
|
|
890
|
-
|
|
891
|
-
// ✅ NEW: Check contextual allowlist if in legal context
|
|
892
|
-
if (isLegalContext && this.context.__verified_intent?.contextual_allowlist) {
|
|
893
|
-
const allowlist = this.context.__verified_intent.contextual_allowlist;
|
|
894
|
-
const triggerWord = match ? match[0].toLowerCase() : '';
|
|
895
|
-
|
|
896
|
-
// Inside the contextual allowlist check in _validateInputs:
|
|
897
|
-
const allowed = allowlist.some(rule => {
|
|
898
|
-
// Check if this pattern's capability matches the rule's trigger
|
|
899
|
-
const triggerMatch =
|
|
900
|
-
triggerWord.includes(rule.trigger.toLowerCase()) ||
|
|
901
|
-
capability.toLowerCase().includes(rule.trigger.toLowerCase()); // ← Handle capability-level triggers
|
|
902
|
-
|
|
903
|
-
if (triggerMatch) {
|
|
904
|
-
// Check if required legal keywords are present
|
|
905
|
-
return rule.requires.some(keyword =>
|
|
906
|
-
text.toLowerCase().includes(keyword.toLowerCase())
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
return false;
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
if (allowed) {
|
|
913
|
-
// ✅ AUDIT LOG: Contextual allowlist bypass
|
|
914
|
-
this._createAuditEntry('safety_bypass', {
|
|
915
|
-
type: 'contextual_allowlist',
|
|
916
|
-
trigger: triggerWord,
|
|
917
|
-
legal_context: true,
|
|
918
|
-
matched_keywords: this.context.__verified_intent.contextual_allowlist
|
|
919
|
-
.find(r => triggerWord.includes(r.trigger.toLowerCase()))?.requires || [],
|
|
920
|
-
severity: 'info'
|
|
1274
|
+
// ── PASS 3: PII block scan (only in block mode — original behaviour) ──
|
|
1275
|
+
if (!redactMode) {
|
|
1276
|
+
for (const field of fieldsToScan) {
|
|
1277
|
+
const text = inputs[field];
|
|
1278
|
+
if (!text || typeof text !== 'string') continue;
|
|
1279
|
+
|
|
1280
|
+
for (const { pattern, capability, lang, label } of this._getPIIPatterns()) {
|
|
1281
|
+
if (pattern.test(text)) {
|
|
1282
|
+
const match = text.match(pattern);
|
|
1283
|
+
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
|
|
1284
|
+
|
|
1285
|
+
this._createAuditEntry('input_safety_violation', {
|
|
1286
|
+
type: 'blocked_input',
|
|
1287
|
+
field: field,
|
|
1288
|
+
detected_phrase: match ? match[0].trim() : 'unknown pattern',
|
|
1289
|
+
capability: capability,
|
|
1290
|
+
language: lang,
|
|
1291
|
+
african_language_detected: isAfrican,
|
|
1292
|
+
financial_expression_found: false,
|
|
1293
|
+
pii_type: label,
|
|
1294
|
+
severity: 'high'
|
|
921
1295
|
});
|
|
922
|
-
|
|
1296
|
+
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
`[O-Lang SAFETY] Blocked PII in field "${field}" (${lang}):\n` +
|
|
1299
|
+
` → Type: ${label}\n` +
|
|
1300
|
+
` → Capability: ${capability}\n` +
|
|
1301
|
+
`\n🛑 Workflow halted before execution. Set OLANG_PII_MODE=redact to auto-redact instead.`
|
|
1302
|
+
);
|
|
923
1303
|
}
|
|
924
1304
|
}
|
|
925
|
-
|
|
926
|
-
// ✅ AUDIT LOG: Input Safety Violation (only if not bypassed)
|
|
927
|
-
this._createAuditEntry('input_safety_violation', {
|
|
928
|
-
type: 'blocked_input',
|
|
929
|
-
field: field,
|
|
930
|
-
detected_phrase: match ? match[0].trim() : 'unknown pattern',
|
|
931
|
-
capability: capability,
|
|
932
|
-
language: lang,
|
|
933
|
-
african_language_detected: isAfrican,
|
|
934
|
-
financial_expression_found: isFinancial,
|
|
935
|
-
legal_context_detected: isLegalContext,
|
|
936
|
-
severity: 'high'
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
throw new Error(
|
|
940
|
-
`[O-Lang SAFETY] Blocked Input in "${lang}":\n` +
|
|
941
|
-
` → Detected: "${match ? match[0].trim() : 'Pattern Match'}"\n` +
|
|
942
|
-
` → Capability: ${capability}\n` +
|
|
943
|
-
` → Field: ${field}\n` +
|
|
944
|
-
` → African Language Detected: ${isAfrican}\n` +
|
|
945
|
-
` → Financial Expression: ${isFinancial}\n` +
|
|
946
|
-
` → Legal Context: ${isLegalContext}\n` +
|
|
947
|
-
`\n🛑 Workflow halted before execution.`
|
|
948
|
-
);
|
|
949
1305
|
}
|
|
950
1306
|
}
|
|
1307
|
+
|
|
1308
|
+
return {
|
|
1309
|
+
passed: true,
|
|
1310
|
+
redactions: Object.keys(allRedactions).length > 0 ? allRedactions : null
|
|
1311
|
+
};
|
|
951
1312
|
}
|
|
952
|
-
return { passed: true };
|
|
953
|
-
}
|
|
954
1313
|
|
|
955
|
-
//
|
|
956
|
-
// ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION
|
|
957
|
-
//
|
|
1314
|
+
// ================================
|
|
1315
|
+
// ✅ UPDATED v1.3.0 — KERNEL-LEVEL LLM HALLUCINATION PREVENTION
|
|
1316
|
+
// (CONJUGATION-AWARE + EVASION-RESISTANT)
|
|
1317
|
+
//
|
|
1318
|
+
// Now uses _getFinancialIntentPatterns() instead of inline duplicate.
|
|
1319
|
+
// Xhosa patterns included automatically via the shared set.
|
|
1320
|
+
// All __verified_intent logic unchanged for backward compat.
|
|
1321
|
+
// ================================
|
|
958
1322
|
_validateLLMOutput(output, actionContext) {
|
|
959
1323
|
if (!output || typeof output !== 'string') return { passed: true };
|
|
960
1324
|
|
|
961
1325
|
// ── __verified_intent takes priority ──────────────────────────────────────
|
|
962
|
-
const intent = this.context.__verified_intent;
|
|
963
|
-
if (intent) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1326
|
+
const intent = this.context.__verified_intent;
|
|
1327
|
+
if (intent) {
|
|
1328
|
+
if (intent.prohibited_actions && Array.isArray(intent.prohibited_actions)) {
|
|
1329
|
+
const lower = output.toLowerCase();
|
|
1330
|
+
for (const action of intent.prohibited_actions) {
|
|
1331
|
+
if (lower.includes(action.toLowerCase())) {
|
|
1332
|
+
return {
|
|
1333
|
+
passed: false,
|
|
1334
|
+
reason: `Output violates prohibited action "${action}" defined in __verified_intent`,
|
|
1335
|
+
detected: action,
|
|
1336
|
+
language: 'multi'
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
974
1340
|
}
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
1341
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1342
|
+
if (intent.prohibited_topics && Array.isArray(intent.prohibited_topics)) {
|
|
1343
|
+
for (const topic of intent.prohibited_topics) {
|
|
1344
|
+
const isRegex = typeof topic === 'object' && topic.pattern;
|
|
1345
|
+
let matched = false;
|
|
1346
|
+
let detected = '';
|
|
1347
|
+
|
|
1348
|
+
if (isRegex) {
|
|
1349
|
+
try {
|
|
1350
|
+
const re = new RegExp(topic.pattern, topic.flags || 'i');
|
|
1351
|
+
const match = output.match(re);
|
|
1352
|
+
matched = !!match;
|
|
1353
|
+
detected = match ? match[0] : topic.pattern;
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
this.addWarning(`Invalid prohibited_topic regex: "${topic.pattern}" — ${e.message}`);
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
} else {
|
|
1359
|
+
matched = output.toLowerCase().includes(topic.toLowerCase());
|
|
1360
|
+
detected = topic;
|
|
1361
|
+
}
|
|
983
1362
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
continue;
|
|
1363
|
+
if (matched) {
|
|
1364
|
+
return {
|
|
1365
|
+
passed: false,
|
|
1366
|
+
reason: `Output violates prohibited topic "${isRegex ? topic.pattern : topic}" defined in __verified_intent`,
|
|
1367
|
+
detected,
|
|
1368
|
+
language: 'multi'
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
993
1371
|
}
|
|
994
|
-
} else {
|
|
995
|
-
matched = output.toLowerCase().includes(topic.toLowerCase());
|
|
996
|
-
detected = topic;
|
|
997
1372
|
}
|
|
998
1373
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
passed: false,
|
|
1002
|
-
reason: `Output violates prohibited topic "${isRegex ? topic.pattern : topic}" defined in __verified_intent`,
|
|
1003
|
-
detected,
|
|
1004
|
-
language: 'multi'
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1374
|
+
// __verified_intent present and passed — skip hardcoded patterns
|
|
1375
|
+
return { passed: true };
|
|
1007
1376
|
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// __verified_intent present and passed — skip hardcoded patterns
|
|
1011
|
-
return { passed: true };
|
|
1012
|
-
}
|
|
1013
1377
|
|
|
1014
|
-
// ── No __verified_intent — fall through to
|
|
1378
|
+
// ── No __verified_intent — fall through to shared pattern set ─────────────
|
|
1015
1379
|
// 🔑 Extract allowed capabilities from workflow allowlist
|
|
1016
1380
|
const allowedCapabilities = Array.from(this.allowedResolvers)
|
|
1017
1381
|
.filter(name => !name.startsWith('llm-') && name !== 'builtInMathResolver')
|
|
1018
1382
|
.map(name => name.replace('@o-lang/', '').replace(/-resolver$/, ''));
|
|
1019
1383
|
|
|
1020
|
-
// 🔒
|
|
1021
|
-
const
|
|
1022
|
-
// ────────────────────────────────────────────────
|
|
1023
|
-
// 🇳🇬 NIGERIAN LANGUAGES
|
|
1024
|
-
// ────────────────────────────────────────────────
|
|
1025
|
-
|
|
1026
|
-
// YORUBA
|
|
1027
|
-
{ pattern: /fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ)/i, capability: 'transfer', lang: 'yo' },
|
|
1028
|
-
{ pattern: /san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo)/i, capability: 'payment', lang: 'yo' },
|
|
1029
|
-
{ pattern: /gba\s+owo/i, capability: 'withdrawal', lang: 'yo' },
|
|
1030
|
-
{ pattern: /fi\s+\w+\s+\w+\s+ranṣẹ/i, capability: 'transfer', lang: 'yo' },
|
|
1031
|
-
{ pattern: /ranṣẹ\s+(?:owo|pesa|kuɗi|ego)/i, capability: 'transfer', lang: 'yo' },
|
|
1032
|
-
{ pattern: /\bti\s+(?:fi|san|gba|da|lo)/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
1033
|
-
{ pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
1034
|
-
{ pattern: /\b(mo\s+ti\s+(?:fi|san|gba))/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
1035
|
-
|
|
1036
|
-
// HAUSA
|
|
1037
|
-
{ pattern: /aika.{0,30}ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
|
|
1038
|
-
{ pattern: /ciyar\s*(?:da)?/i, capability: 'transfer', lang: 'ha' },
|
|
1039
|
-
{ pattern: /shiga\s+ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
|
|
1040
|
-
{ pattern: /turo\s+.*\s+aika/i, capability: 'transfer', lang: 'ha' },
|
|
1041
|
-
{ pattern: /biya\s*(?:da)?/i, capability: 'payment', lang: 'ha' },
|
|
1042
|
-
{ pattern: /sahaw[ae]\s+ku(?:ɗ|d)i/iu, capability: 'withdrawal', lang: 'ha' },
|
|
1043
|
-
{ pattern: /(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
1044
|
-
{ pattern: /(?:za\s+a|za\s+ta)\s+(?:ciyar|biya)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
1045
|
-
{ pattern: /ina\s+(?:ciyar|biya|sahawa)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
1046
|
-
|
|
1047
|
-
// IGBO
|
|
1048
|
-
{ pattern: /zipu\s+(?:ego|moni|isi|na)/i, capability: 'transfer', lang: 'ig' },
|
|
1049
|
-
{ pattern: /buru\s+(?:ego|moni|isi)/i, capability: 'transfer', lang: 'ig' },
|
|
1050
|
-
{ pattern: /zi\s+.*\s+zipu/i, capability: 'transfer', lang: 'ig' },
|
|
1051
|
-
{ pattern: /tinye\s+(?:ego|moni|isi)/i, capability: 'deposit', lang: 'ig' },
|
|
1052
|
-
{ pattern: /(?:ziri|bururu|tinyere|gbara)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
1053
|
-
{ pattern: /m\s+(?:ziri|buru|zipuru|tinyere)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
1054
|
-
|
|
1055
|
-
// SWAHILI
|
|
1056
|
-
{ pattern: /tuma\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
|
|
1057
|
-
{ pattern: /pelek[ae]?\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
|
|
1058
|
-
{ pattern: /wasilisha/i, capability: 'transfer', lang: 'sw' },
|
|
1059
|
-
{ pattern: /\b\w*lip[ae]\w*/i, capability: 'payment', lang: 'sw' },
|
|
1060
|
-
{ pattern: /maliza\s+malipo/i, capability: 'payment', lang: 'sw' },
|
|
1061
|
-
{ pattern: /ongez[ae]?\s*(?:kiasi|pesa|fedha)/i, capability: 'deposit', lang: 'sw' },
|
|
1062
|
-
{ pattern: /wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa)/i, capability: 'deposit', lang: 'sw' },
|
|
1063
|
-
{ pattern: /nime(?:tuma|lipa|ongeza|weka|peleka)/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
1064
|
-
{ pattern: /(?:ni|u|a|tu|m|wa|ki|vi|zi|i)\s*me\s*(?:ongeza|weka|tuma|peleka|lipa|wasilisha)/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
1065
|
-
|
|
1066
|
-
// ────────────────────────────────────────────────
|
|
1067
|
-
// 🌍 OTHER AFRICAN LANGUAGES
|
|
1068
|
-
// ────────────────────────────────────────────────
|
|
1069
|
-
|
|
1070
|
-
// AMHARIC - Unicode escapes to avoid encoding issues
|
|
1071
|
-
{ pattern: /\u120b\u12ad/u, capability: 'transfer', lang: 'am' },
|
|
1072
|
-
{ pattern: /\u1308\u1263/u, capability: 'deposit', lang: 'am' },
|
|
1073
|
-
{ pattern: /\u12ad\u134c\u120d/u, capability: 'payment', lang: 'am' },
|
|
1074
|
-
{ pattern: /[\u1200-\u137F]{0,4}(?:\u1270\u120b\u120b\u1348|\u120b\u12ad|\u12ad\u134c\u120d|\u1338\u121d\u122d|\u12c8\u1323|\u1308\u1263)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
|
|
1075
|
-
|
|
1076
|
-
// SOMALI
|
|
1077
|
-
{ pattern: /dir\s+(?:lacag|maal|qarsoon)/i, capability: 'transfer', lang: 'so' },
|
|
1078
|
-
{ pattern: /bixi|bixis\s*o/i, capability: 'payment', lang: 'so' },
|
|
1079
|
-
|
|
1080
|
-
// ZULU
|
|
1081
|
-
{ pattern: /thumel/i, capability: 'transfer', lang: 'zu' },
|
|
1082
|
-
{ pattern: /thumel.*imali/i, capability: 'transfer', lang: 'zu' },
|
|
1083
|
-
{ pattern: /hlawul/i, capability: 'payment', lang: 'zu' },
|
|
1084
|
-
{ pattern: /hlawul.*imali/i, capability: 'payment', lang: 'zu' },
|
|
1085
|
-
|
|
1086
|
-
// ────────────────────────────────────────────────
|
|
1087
|
-
// 🌐 GLOBAL LANGUAGES
|
|
1088
|
-
// ────────────────────────────────────────────────
|
|
1089
|
-
{ pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
1090
|
-
{ pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
1091
|
-
{ pattern: /\b(transfer(?:red|ring)?|send(?:ing)?|wire(?:d)?|pay(?:ed|ing)?|withdraw(?:n)?|deposit(?:ed|ing)?|disburse(?:d)?)\b/i, capability: 'financial_action', lang: 'en' },
|
|
1092
|
-
{ pattern: /\bI\s+(?:can|will|am able to|have|'ve|did|already)\s+(?:transfer|send|pay|withdraw|deposit|wire)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
1093
|
-
{ pattern: /\b(?:j'?ai|tu as|il a|elle a|nous avons|vous avez|ils ont|elles ont)\s+(?:viré|transféré|envoyé|payé|retiré|déposé)\b/i, capability: 'unauthorized_action', lang: 'fr' },
|
|
1094
|
-
{ pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
|
|
1095
|
-
{ pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}(?:ت|نا|تم|تا|تِ|تُ|تَ)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
|
|
1096
|
-
{ pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
|
|
1097
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|支付|存款|取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
1098
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|转帐|支付|付款|提款|取款|存款|存入|汇款|存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
1099
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:我|已|已经)\s*(?:转账|支付|提款|存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
|
|
1100
|
-
|
|
1101
|
-
// ────────────────────────────────────────────────
|
|
1102
|
-
// 🛡️ PII & EVASION
|
|
1103
|
-
// ────────────────────────────────────────────────
|
|
1104
|
-
{ pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
|
|
1105
|
-
{ pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/i, capability: 'pii_exposure', lang: 'multi' },
|
|
1106
|
-
{ pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})/i, capability: 'pii_exposure', lang: 'multi' },
|
|
1107
|
-
{ pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|accepted|verified|imethibitishwa|imefanikiwa|amthibitishwa|ti\s+da|ti\s+ṣe|gụnyere|kimefanyika|yamekamilika)\b/i, capability: 'deceptive_claim', lang: 'multi' },
|
|
1108
|
-
];
|
|
1109
|
-
|
|
1110
|
-
// 🔍 SCAN OUTPUT FOR FORBIDDEN INTENTS
|
|
1111
|
-
for (const { pattern, capability, lang } of forbiddenPatterns) {
|
|
1384
|
+
// 🔒 SCAN OUTPUT FOR FORBIDDEN INTENTS (shared set — includes Xhosa)
|
|
1385
|
+
for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
|
|
1112
1386
|
if (pattern.test(output)) {
|
|
1113
1387
|
const hasCapability = allowedCapabilities.some(c =>
|
|
1114
1388
|
c.includes(capability) ||
|
|
@@ -1121,9 +1395,9 @@ const forbiddenPatterns = [
|
|
|
1121
1395
|
|
|
1122
1396
|
if (!hasCapability) {
|
|
1123
1397
|
const match = output.match(pattern);
|
|
1124
|
-
|
|
1398
|
+
|
|
1125
1399
|
// ✅ Explicitly flag African & Financial context for Audit Logs
|
|
1126
|
-
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'am', 'om', 'ff', 'so', 'sn'].includes(lang);
|
|
1400
|
+
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
|
|
1127
1401
|
const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
|
|
1128
1402
|
|
|
1129
1403
|
return {
|
|
@@ -1131,9 +1405,9 @@ const forbiddenPatterns = [
|
|
|
1131
1405
|
reason: `Hallucinated "${capability}" capability in ${lang}...`,
|
|
1132
1406
|
detected: match ? match[0].trim() : 'unknown pattern',
|
|
1133
1407
|
language: lang,
|
|
1134
|
-
african_language_detected: isAfrican,
|
|
1408
|
+
african_language_detected: isAfrican,
|
|
1135
1409
|
financial_expression_found: isFinancial,
|
|
1136
|
-
capability_attempted: capability
|
|
1410
|
+
capability_attempted: capability
|
|
1137
1411
|
};
|
|
1138
1412
|
}
|
|
1139
1413
|
}
|
|
@@ -1144,22 +1418,22 @@ const forbiddenPatterns = [
|
|
|
1144
1418
|
// -----------------------------
|
|
1145
1419
|
// ✅ CRITICAL FIX: Resolver output unwrapping helper
|
|
1146
1420
|
// -----------------------------
|
|
1147
|
-
_unwrapResolverResult(result) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1421
|
+
_unwrapResolverResult(result) {
|
|
1422
|
+
if (result && typeof result === 'object') {
|
|
1423
|
+
if (result.output !== undefined) return result.output;
|
|
1424
|
+
if (result.response !== undefined) return result.response; // ✅ ADD THIS
|
|
1425
|
+
if (result.text !== undefined) return result.text;
|
|
1426
|
+
if (result.content !== undefined) return result.content;
|
|
1427
|
+
}
|
|
1428
|
+
return result;
|
|
1153
1429
|
}
|
|
1154
|
-
return result;
|
|
1155
|
-
}
|
|
1156
1430
|
|
|
1157
1431
|
// -----------------------------
|
|
1158
1432
|
// Step execution (WHERE RESOLVERS ARE INVOKED)
|
|
1159
1433
|
// -----------------------------
|
|
1160
1434
|
async executeStep(step, agentResolver) {
|
|
1161
1435
|
const stepType = step.type;
|
|
1162
|
-
|
|
1436
|
+
|
|
1163
1437
|
// ✅ Enforce per-step constraints (basic validation)
|
|
1164
1438
|
if (step.constraints && Object.keys(step.constraints).length > 0) {
|
|
1165
1439
|
for (const [key, value] of Object.entries(step.constraints)) {
|
|
@@ -1235,7 +1509,7 @@ _unwrapResolverResult(result) {
|
|
|
1235
1509
|
this.context[`__resolver_${idx}`] = result;
|
|
1236
1510
|
return this._unwrapResolverResult(result);
|
|
1237
1511
|
}
|
|
1238
|
-
|
|
1512
|
+
|
|
1239
1513
|
resolverAttempts.push({
|
|
1240
1514
|
name: resolverName,
|
|
1241
1515
|
status: 'skipped',
|
|
@@ -1303,7 +1577,7 @@ _unwrapResolverResult(result) {
|
|
|
1303
1577
|
errorMessage += ` → Verify API keys/tokens are set in environment variables\n`;
|
|
1304
1578
|
}
|
|
1305
1579
|
}
|
|
1306
|
-
|
|
1580
|
+
|
|
1307
1581
|
errorMessage += `\n• Resolver documentation:\n`;
|
|
1308
1582
|
let hasDocs = false;
|
|
1309
1583
|
resolverAttempts.forEach(attempt => {
|
|
@@ -1319,7 +1593,7 @@ _unwrapResolverResult(result) {
|
|
|
1319
1593
|
throw new Error(errorMessage);
|
|
1320
1594
|
};
|
|
1321
1595
|
|
|
1322
|
-
|
|
1596
|
+
switch (stepType) {
|
|
1323
1597
|
case 'calculate': {
|
|
1324
1598
|
// ✅ Interpolate variables in the expression before evaluation
|
|
1325
1599
|
let expr = step.expression || step.actionRaw;
|
|
@@ -1330,7 +1604,7 @@ _unwrapResolverResult(result) {
|
|
|
1330
1604
|
if (step.saveAs) this.context[step.saveAs] = result;
|
|
1331
1605
|
break;
|
|
1332
1606
|
}
|
|
1333
|
-
|
|
1607
|
+
|
|
1334
1608
|
case 'action': {
|
|
1335
1609
|
let action = this._safeInterpolate(
|
|
1336
1610
|
step.actionRaw,
|
|
@@ -1428,7 +1702,7 @@ _unwrapResolverResult(result) {
|
|
|
1428
1702
|
}
|
|
1429
1703
|
break;
|
|
1430
1704
|
}
|
|
1431
|
-
|
|
1705
|
+
|
|
1432
1706
|
case 'use': {
|
|
1433
1707
|
const tool = this._safeInterpolate(step.tool, this.context, 'tool name');
|
|
1434
1708
|
const rawResult = await runResolvers(`Use ${tool}`);
|
|
@@ -1436,7 +1710,7 @@ _unwrapResolverResult(result) {
|
|
|
1436
1710
|
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
1437
1711
|
break;
|
|
1438
1712
|
}
|
|
1439
|
-
|
|
1713
|
+
|
|
1440
1714
|
case 'ask': {
|
|
1441
1715
|
const target = this._safeInterpolate(step.target, this.context, 'LLM prompt');
|
|
1442
1716
|
if (/{[^}]+}/.test(target)) {
|
|
@@ -1461,7 +1735,7 @@ _unwrapResolverResult(result) {
|
|
|
1461
1735
|
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
1462
1736
|
break;
|
|
1463
1737
|
}
|
|
1464
|
-
|
|
1738
|
+
|
|
1465
1739
|
case 'evolve': {
|
|
1466
1740
|
const { targetResolver, feedback } = step;
|
|
1467
1741
|
if (this.verbose) {
|
|
@@ -1483,14 +1757,14 @@ _unwrapResolverResult(result) {
|
|
|
1483
1757
|
}
|
|
1484
1758
|
break;
|
|
1485
1759
|
}
|
|
1486
|
-
|
|
1760
|
+
|
|
1487
1761
|
case 'if': {
|
|
1488
1762
|
if (this.evaluateCondition(step.condition, this.context)) {
|
|
1489
1763
|
for (const s of step.body) await this.executeStep(s, agentResolver);
|
|
1490
1764
|
}
|
|
1491
1765
|
break;
|
|
1492
1766
|
}
|
|
1493
|
-
|
|
1767
|
+
|
|
1494
1768
|
case 'parallel': {
|
|
1495
1769
|
const { steps, timeout } = step;
|
|
1496
1770
|
if (timeout !== undefined && timeout > 0) {
|
|
@@ -1514,7 +1788,7 @@ _unwrapResolverResult(result) {
|
|
|
1514
1788
|
}
|
|
1515
1789
|
break;
|
|
1516
1790
|
}
|
|
1517
|
-
|
|
1791
|
+
|
|
1518
1792
|
case 'escalation': {
|
|
1519
1793
|
const { levels } = step;
|
|
1520
1794
|
let finalResult = null;
|
|
@@ -1566,17 +1840,17 @@ _unwrapResolverResult(result) {
|
|
|
1566
1840
|
}
|
|
1567
1841
|
break;
|
|
1568
1842
|
}
|
|
1569
|
-
|
|
1843
|
+
|
|
1570
1844
|
case 'connect': {
|
|
1571
1845
|
this.resources[step.resource] = step.endpoint;
|
|
1572
1846
|
break;
|
|
1573
1847
|
}
|
|
1574
|
-
|
|
1848
|
+
|
|
1575
1849
|
case 'agent_use': {
|
|
1576
1850
|
this.agentMap[step.logicalName] = step.resource;
|
|
1577
1851
|
break;
|
|
1578
1852
|
}
|
|
1579
|
-
|
|
1853
|
+
|
|
1580
1854
|
case 'debrief': {
|
|
1581
1855
|
if (step.message.includes('{')) {
|
|
1582
1856
|
const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
|
|
@@ -1588,14 +1862,14 @@ _unwrapResolverResult(result) {
|
|
|
1588
1862
|
this.emit('debrief', { agent: step.agent, message: step.message });
|
|
1589
1863
|
break;
|
|
1590
1864
|
}
|
|
1591
|
-
|
|
1865
|
+
|
|
1592
1866
|
case 'prompt': {
|
|
1593
1867
|
if (this.verbose) {
|
|
1594
1868
|
console.log(`❓ Prompt: ${step.question}`);
|
|
1595
1869
|
}
|
|
1596
1870
|
break;
|
|
1597
1871
|
}
|
|
1598
|
-
|
|
1872
|
+
|
|
1599
1873
|
case 'emit': {
|
|
1600
1874
|
const payloadTemplate = step.payload;
|
|
1601
1875
|
const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
|
|
@@ -1623,7 +1897,7 @@ _unwrapResolverResult(result) {
|
|
|
1623
1897
|
}
|
|
1624
1898
|
break;
|
|
1625
1899
|
}
|
|
1626
|
-
|
|
1900
|
+
|
|
1627
1901
|
case 'persist': {
|
|
1628
1902
|
if (!this._requireSemantic(step.variable, 'persist')) {
|
|
1629
1903
|
if (this.verbose) {
|
|
@@ -1649,7 +1923,7 @@ _unwrapResolverResult(result) {
|
|
|
1649
1923
|
}
|
|
1650
1924
|
break;
|
|
1651
1925
|
}
|
|
1652
|
-
|
|
1926
|
+
|
|
1653
1927
|
case 'persist-db': {
|
|
1654
1928
|
if (!this.dbClient) {
|
|
1655
1929
|
this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
|
|
@@ -1682,7 +1956,7 @@ _unwrapResolverResult(result) {
|
|
|
1682
1956
|
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
1683
1957
|
await db.collection(step.collection).insertOne({
|
|
1684
1958
|
workflow_name: this.context.workflow_name || 'unknown',
|
|
1685
|
-
|
|
1959
|
+
sourceValue,
|
|
1686
1960
|
created_at: new Date()
|
|
1687
1961
|
});
|
|
1688
1962
|
break;
|
|
@@ -1717,18 +1991,20 @@ _unwrapResolverResult(result) {
|
|
|
1717
1991
|
if (workflow.type !== 'workflow') {
|
|
1718
1992
|
throw new Error(`Unknown workflow type: ${workflow.type}`);
|
|
1719
1993
|
}
|
|
1720
|
-
|
|
1994
|
+
|
|
1721
1995
|
this.context = {
|
|
1722
1996
|
...inputs,
|
|
1723
1997
|
workflow_name: workflow.name
|
|
1724
1998
|
};
|
|
1725
1999
|
|
|
1726
|
-
|
|
1727
|
-
|
|
2000
|
+
// ✅ NEW: Validate Inputs BEFORE any step runs
|
|
2001
|
+
// In redact mode: PII tokens are replaced in place, execution continues.
|
|
2002
|
+
// In block mode (default): throws on any PII or financial intent match.
|
|
2003
|
+
this._validateInputs(inputs);
|
|
1728
2004
|
|
|
1729
2005
|
// ✅ AUDIT LOG: Workflow start (ENHANCED with governance metadata)
|
|
1730
2006
|
const governanceHash = this._generateGovernanceProfileHash(workflow);
|
|
1731
|
-
|
|
2007
|
+
|
|
1732
2008
|
this._createAuditEntry('workflow_started', {
|
|
1733
2009
|
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`, // ✅ Workflow ID
|
|
1734
2010
|
workflow_name: workflow.name,
|
|
@@ -1739,6 +2015,7 @@ _unwrapResolverResult(result) {
|
|
|
1739
2015
|
inputs_count: Object.keys(inputs).length,
|
|
1740
2016
|
steps_count: workflow.steps.length,
|
|
1741
2017
|
allowed_resolvers: workflow.allowedResolvers || [],
|
|
2018
|
+
pii_mode: this.piiMode, // ✅ NEW v1.3.0 — surfaced in audit
|
|
1742
2019
|
constraints: {
|
|
1743
2020
|
max_generations: workflow.maxGenerations,
|
|
1744
2021
|
strict_inputs: process.env.OLANG_STRICT_INPUTS === 'true'
|
|
@@ -1773,7 +2050,7 @@ _unwrapResolverResult(result) {
|
|
|
1773
2050
|
for (const step of workflow.steps) {
|
|
1774
2051
|
await this.executeStep(step, agentResolver);
|
|
1775
2052
|
}
|
|
1776
|
-
|
|
2053
|
+
|
|
1777
2054
|
// ✅ AUDIT LOG: Workflow completion (ENHANCED)
|
|
1778
2055
|
this._createAuditEntry('workflow_completed', {
|
|
1779
2056
|
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`,
|