@o-lang/olang 1.2.38 → 1.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -11
- package/src/parser/index.js +187 -258
- package/src/runtime/RuntimeAPI.js +1324 -473
- 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.4.0-alpha.1'; // 🔁 Bumped: PII redaction engine added
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// ✅ NEW v1.3.0 — SEPARATED PATTERN SETS
|
|
10
|
+
//
|
|
11
|
+
// WHY: Previously _validateInputs and _validateLLMOutput both duplicated one
|
|
12
|
+
// giant flat list. PII redaction must ONLY replace PII tokens (phone numbers,
|
|
13
|
+
// BVNs, account numbers) — it must NOT replace financial intent phrases like
|
|
14
|
+
// "fi owo ranṣẹ" with "[TRANSFER_REDACTED]" in legitimate LLM prompts.
|
|
15
|
+
//
|
|
16
|
+
// Backward compatibility: all old patterns are preserved exactly. They are now
|
|
17
|
+
// organised into two methods:
|
|
18
|
+
// _getPIIPatterns() → used by new _redactPII()
|
|
19
|
+
// _getFinancialIntentPatterns() → used by _validateInputs / _validateLLMOutput
|
|
20
|
+
//
|
|
21
|
+
// BACKWARD COMPAT: _validateInputs still throws by default (MODE = 'block').
|
|
22
|
+
// Set OLANG_PII_MODE=redact in env to switch to non-throwing redaction mode.
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
24
|
|
|
8
25
|
class RuntimeAPI {
|
|
9
26
|
constructor({ verbose = false } = {}) {
|
|
@@ -20,24 +37,362 @@ class RuntimeAPI {
|
|
|
20
37
|
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
|
21
38
|
this.disallowedLogFile = path.join(logsDir, 'disallowed_resolvers.json');
|
|
22
39
|
this.disallowedAttempts = [];
|
|
23
|
-
|
|
40
|
+
|
|
24
41
|
// ✅ NEW: Database client setup
|
|
25
42
|
this.dbClient = null;
|
|
26
43
|
this._initDbClient();
|
|
27
|
-
|
|
44
|
+
|
|
28
45
|
// ✅ NEW: Cryptographically verifiable audit logs
|
|
29
46
|
this.auditLog = [];
|
|
30
47
|
this.previousHash = 'GENESIS';
|
|
31
48
|
this.auditLogPrivateKey = process.env.OLANG_AUDIT_PRIVATE_KEY;
|
|
32
49
|
this.auditLogFile = path.join(logsDir, 'audit_log.json');
|
|
33
50
|
this.enableAuditLog = process.env.OLANG_AUDIT_LOG === 'true';
|
|
34
|
-
|
|
51
|
+
|
|
52
|
+
// ✅ NEW v1.3.0 — PII operating mode
|
|
53
|
+
// 'block' → original behaviour: throw on PII (default, backward compat)
|
|
54
|
+
// 'redact' → new behaviour: replace PII tokens, continue execution
|
|
55
|
+
// 'redact-and-log' → redact + emit audit entry per redaction event
|
|
56
|
+
this.piiMode = process.env.OLANG_PII_MODE || 'block';
|
|
57
|
+
|
|
35
58
|
if (this.enableAuditLog && this.verbose) {
|
|
36
59
|
console.log('🔐 Cryptographically verifiable audit logging enabled');
|
|
37
60
|
}
|
|
61
|
+
|
|
62
|
+
if (this.verbose && this.piiMode !== 'block') {
|
|
63
|
+
console.log(`🛡️ PII mode: ${this.piiMode}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ================================
|
|
68
|
+
// ✅ NEW v1.3.0 — PII-ONLY PATTERN SET
|
|
69
|
+
//
|
|
70
|
+
// These patterns match concrete identifiers that can be replaced with a
|
|
71
|
+
// [TYPE_REDACTED] token without destroying the semantic meaning of a sentence.
|
|
72
|
+
// They cover every language listed on the O-Lang site.
|
|
73
|
+
// ================================
|
|
74
|
+
|
|
75
|
+
_getPIIPatterns() {
|
|
76
|
+
return [
|
|
77
|
+
|
|
78
|
+
// ── Phone Numbers ──────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
// Nigeria (MTN, Airtel, Glo, 9mobile)
|
|
81
|
+
{
|
|
82
|
+
pattern: /\b(?:\+?234\s*[-.]?|0)(?:70|80|81|90|91)\d{8}\b/g,
|
|
83
|
+
capability: 'pii_phone',
|
|
84
|
+
lang: 'ng',
|
|
85
|
+
label: 'NG_PHONE'
|
|
86
|
+
},
|
|
87
|
+
// Kenya (+254 / 07xx / 01xx)
|
|
88
|
+
{
|
|
89
|
+
pattern: /\b(?:\+?254\s*[-.]?|0)(?:7[0-9]|1[01])\d{7}\b/g,
|
|
90
|
+
capability: 'pii_phone',
|
|
91
|
+
lang: 'ke',
|
|
92
|
+
label: 'KE_PHONE'
|
|
93
|
+
},
|
|
94
|
+
// South Africa (+27 / 0xx)
|
|
95
|
+
{
|
|
96
|
+
pattern: /\b(?:\+?27\s*[-.]?|0)[6-8]\d{8}\b/g,
|
|
97
|
+
capability: 'pii_phone',
|
|
98
|
+
lang: 'za',
|
|
99
|
+
label: 'ZA_PHONE'
|
|
100
|
+
},
|
|
101
|
+
// Ethiopia (+251)
|
|
102
|
+
{
|
|
103
|
+
pattern: /\b(?:\+?251\s*[-.]?|0)[79]\d{8}\b/g,
|
|
104
|
+
capability: 'pii_phone',
|
|
105
|
+
lang: 'et',
|
|
106
|
+
label: 'ET_PHONE'
|
|
107
|
+
},
|
|
108
|
+
// 🇬🇭 Ghana (+233) - MTN (24/54), Telecel (20/50), AT (26/56), Glo (23)
|
|
109
|
+
{
|
|
110
|
+
pattern: /\b(?:\+?233\s*[-.]?|0)(?:2[0346]|5[046])\d{7}\b/g,
|
|
111
|
+
capability: 'pii_phone',
|
|
112
|
+
lang: 'gh',
|
|
113
|
+
label: 'GH_PHONE'
|
|
114
|
+
},
|
|
115
|
+
// Generic international E.164
|
|
116
|
+
{
|
|
117
|
+
pattern: /\+(?!234|254|27\b|251|233)[1-9]\d{1,2}[-.\s]?\d{3,5}[-.\s]?\d{4,9}\b/g,
|
|
118
|
+
capability: 'pii_phone',
|
|
119
|
+
lang: 'intl',
|
|
120
|
+
label: 'INTL_PHONE'
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// ── National Identity Numbers ──────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
// Nigeria BVN (11 digits)
|
|
126
|
+
{
|
|
127
|
+
pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/ig,
|
|
128
|
+
capability: 'pii_national_id',
|
|
129
|
+
lang: 'ng',
|
|
130
|
+
label: 'NG_BVN'
|
|
131
|
+
},
|
|
132
|
+
// Nigeria NIN (11 digits)
|
|
133
|
+
{
|
|
134
|
+
pattern: /\b(?:nin|national\s+identification\s+number)\b.{0,20}\d{11}/ig,
|
|
135
|
+
capability: 'pii_national_id',
|
|
136
|
+
lang: 'ng',
|
|
137
|
+
label: 'NG_NIN'
|
|
138
|
+
},
|
|
139
|
+
// 🇬🇭 Ghana Card (National ID) - Format: GHA-XXXXXXXXX-X
|
|
140
|
+
{
|
|
141
|
+
pattern: /\bGHA-\d{9}-\d\b/ig,
|
|
142
|
+
capability: 'pii_national_id',
|
|
143
|
+
lang: 'gh',
|
|
144
|
+
label: 'GH_CARD'
|
|
145
|
+
},
|
|
146
|
+
// South Africa ID (13 digits YYMMDD + gender + race + check)
|
|
147
|
+
{
|
|
148
|
+
pattern: /\b[0-9]{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01])[0-9]{4}[01][0-9]{2}\b/g,
|
|
149
|
+
capability: 'pii_national_id',
|
|
150
|
+
lang: 'za',
|
|
151
|
+
label: 'ZA_ID'
|
|
152
|
+
},
|
|
153
|
+
// Kenya Huduma / National ID (7-8 digits)
|
|
154
|
+
{
|
|
155
|
+
pattern: /\b(?:national\s+id|id\s+number|huduma\s+namba)\b.{0,10}\d{7,8}\b/ig,
|
|
156
|
+
capability: 'pii_national_id',
|
|
157
|
+
lang: 'ke',
|
|
158
|
+
label: 'KE_ID'
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// ── Bank Account Numbers ───────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
// Generic account reference (works across all listed languages)
|
|
164
|
+
// Added Twi/Akan terms: 'sika' (money), 'konte' (account)
|
|
165
|
+
{
|
|
166
|
+
pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#|compte|cuenta|konto|konte|sika\s+number|حساب|حساب\s+رقم|حسابي|akaunti\s+ya|nambari\s+ya\s+akaunti)\s*[:\-—–]?\s*(\d{6,18})\b/ig,
|
|
167
|
+
capability: 'pii_account',
|
|
168
|
+
lang: 'multi',
|
|
169
|
+
label: 'ACCOUNT_NUMBER'
|
|
170
|
+
},
|
|
171
|
+
// IBAN (EU + Africa SWIFT members)
|
|
172
|
+
{
|
|
173
|
+
pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b/g,
|
|
174
|
+
capability: 'pii_account',
|
|
175
|
+
lang: 'intl',
|
|
176
|
+
label: 'IBAN'
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// ── Email Addresses ────────────────────────────────────────────────────
|
|
180
|
+
{
|
|
181
|
+
pattern: /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/g,
|
|
182
|
+
capability: 'pii_email',
|
|
183
|
+
lang: 'multi',
|
|
184
|
+
label: 'EMAIL'
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// ── Deceptive completion claims (cross-language) ───────────────────────
|
|
188
|
+
// These belong in PII set because they mask fraudulent state
|
|
189
|
+
{
|
|
190
|
+
pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|verified|imethibitishwa|imefanikiwa|amthibitishwa|ti\s+da|ti\s+ṣe|gụnyere|kimefanyika|yamekamilika)\b/ig,
|
|
191
|
+
capability: 'pii_deceptive_claim',
|
|
192
|
+
lang: 'multi',
|
|
193
|
+
label: 'DECEPTIVE_CLAIM'
|
|
194
|
+
}
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
// ================================
|
|
198
|
+
// ✅ NEW v1.3.0 — FINANCIAL INTENT PATTERN SET (unchanged from v1.2.x)
|
|
199
|
+
//
|
|
200
|
+
// These patterns detect *intent* to perform a financial action. They are
|
|
201
|
+
// NOT suitable for token-level redaction because replacing "fi owo ranṣẹ"
|
|
202
|
+
// with "[TRANSFER_REDACTED]" would destroy the sentence. They are used by
|
|
203
|
+
// _validateInputs (block mode) and _validateLLMOutput (always blocks).
|
|
204
|
+
// ================================
|
|
205
|
+
|
|
206
|
+
_getFinancialIntentPatterns() {
|
|
207
|
+
return [
|
|
208
|
+
|
|
209
|
+
// ────────────────────────────────────────────────
|
|
210
|
+
// 🇳🇬 NIGERIAN LANGUAGES (Fixed Unicode Boundaries)
|
|
211
|
+
// ────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
// YORUBA: Removed trailing \b after 'ṣẹ' to fix Unicode matching
|
|
214
|
+
{ pattern: /fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ)/i, capability: 'transfer', lang: 'yo' },
|
|
215
|
+
{ pattern: /ranṣẹ\s+(?:owo|pesa|kuɗi|ego)/i, capability: 'transfer', lang: 'yo' },
|
|
216
|
+
{ pattern: /fi\s+\w+\s+\w+\s+ranṣẹ/i, capability: 'transfer', lang: 'yo' }, // Catches "Fi 5000 naira ranṣẹ"
|
|
217
|
+
{ pattern: /san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo)/i, capability: 'payment', lang: 'yo' },
|
|
218
|
+
{ pattern: /gba\s+owo/i, capability: 'withdrawal', lang: 'yo' },
|
|
219
|
+
{ pattern: /\bti\s+(?:fi|san|gba|da|lo)/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
220
|
+
{ pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
221
|
+
{ pattern: /\b(mo\s+ti\s+(?:fi|san|gba))/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
222
|
+
|
|
223
|
+
// HAUSA: ✅ FIXED - Aggressive Substring Match (No Boundaries)
|
|
224
|
+
{ pattern: /aika.{0,30}ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
|
|
225
|
+
{ pattern: /ciyar\s*(?:da)?/i, capability: 'transfer', lang: 'ha' },
|
|
226
|
+
{ pattern: /shiga\s+ku(?:ɗ|d)i/iu, capability: 'transfer', lang: 'ha' },
|
|
227
|
+
{ pattern: /turo\s+.*\s+aika/i, capability: 'transfer', lang: 'ha' },
|
|
228
|
+
{ pattern: /biya\s*(?:da)?/i, capability: 'payment', lang: 'ha' },
|
|
229
|
+
{ pattern: /sahaw[ae]\s+ku(?:ɗ|d)i/iu, capability: 'withdrawal', lang: 'ha' },
|
|
230
|
+
{ pattern: /(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
231
|
+
{ pattern: /(?:za\s+a|za\s+ta)\s+(?:ciyar|biya)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
232
|
+
{ pattern: /ina\s+(?:ciyar|biya|sahawa)/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
233
|
+
|
|
234
|
+
// IGBO: Removed trailing \b after 'igo'
|
|
235
|
+
{ pattern: /zipu\s+(?:ego|moni|isi|na)/i, capability: 'transfer', lang: 'ig' },
|
|
236
|
+
{ pattern: /buru\s+(?:ego|moni|isi)/i, capability: 'transfer', lang: 'ig' },
|
|
237
|
+
{ pattern: /zi\s+.*\s+zipu/i, capability: 'transfer', lang: 'ig' },
|
|
238
|
+
{ pattern: /tinye\s+(?:ego|moni|isi)/i, capability: 'deposit', lang: 'ig' },
|
|
239
|
+
{ pattern: /(?:ziri|bururu|tinyere|gbara)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
240
|
+
{ pattern: /m\s+(?:ziri|buru|zipuru|tinyere)/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
241
|
+
|
|
242
|
+
// SWAHILI: ✅ FIXED - Catch Conjugated Forms (ni-li-pe, a-li-pe)
|
|
243
|
+
{ pattern: /tuma\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
|
|
244
|
+
{ pattern: /pelek[ae]?\s+(?:pesa|fedha)/i, capability: 'transfer', lang: 'sw' },
|
|
245
|
+
{ pattern: /wasilisha/i, capability: 'transfer', lang: 'sw' },
|
|
246
|
+
{ pattern: /\b\w*lip[ae]\w*/i, capability: 'payment', lang: 'sw' },
|
|
247
|
+
{ pattern: /maliza\s+malipo/i, capability: 'payment', lang: 'sw' },
|
|
248
|
+
{ pattern: /ongez[ae]?\s*(?:kiasi|pesa|fedha)/i, capability: 'deposit', lang: 'sw' },
|
|
249
|
+
{ pattern: /wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa)/i, capability: 'deposit', lang: 'sw' },
|
|
250
|
+
{ pattern: /nime(?:tuma|lipa|ongeza|weka|peleka)/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
251
|
+
{ pattern: /(?:ni|u|a|tu|m|wa|ki|vi|zi|i)\s*me\s*(?:ongeza|weka|tuma|peleka|lipa|wasilisha)/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
252
|
+
|
|
253
|
+
// OTHER AFRICAN:
|
|
254
|
+
// Amharic: Match roots anywhere
|
|
255
|
+
{ pattern: /\u120b\u12ad/u, capability: 'transfer', lang: 'am' },
|
|
256
|
+
{ pattern: /\u1308\u1263/u, capability: 'deposit', lang: 'am' },
|
|
257
|
+
{ pattern: /\u12ad\u134c\u120d/u, capability: 'payment', lang: 'am' },
|
|
258
|
+
{ pattern: /[\u1200-\u137F]{0,4}(?:\u1270\u120b\u120b\u1348|\u120b\u12ad|\u12ad\u134c\u120d|\u1338\u121d\u122d|\u12c8\u1323|\u1308\u1263)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
|
|
259
|
+
|
|
260
|
+
// Somali
|
|
261
|
+
{ pattern: /dir\s+(?:lacag|maal|qarsoon)/i, capability: 'transfer', lang: 'so' },
|
|
262
|
+
{ pattern: /bixi|bixis\s*o/i, capability: 'payment', lang: 'so' },
|
|
263
|
+
|
|
264
|
+
// Zulu: ✅ FIXED - Handle Subject Concords (u-thumela, ngi-hlawule)
|
|
265
|
+
{ pattern: /thumel/i, capability: 'transfer', lang: 'zu' }, // Matches root inside uthumela, ngithumela
|
|
266
|
+
{ pattern: /thumel.*imali/i, capability: 'transfer', lang: 'zu' },
|
|
267
|
+
{ pattern: /hlawul/i, capability: 'payment', lang: 'zu' }, // Matches root inside hlawula, ngihlawule
|
|
268
|
+
{ pattern: /hlawul.*imali/i, capability: 'payment', lang: 'zu' },
|
|
269
|
+
|
|
270
|
+
// ────────────────────────────────────────────────
|
|
271
|
+
// 🇿🇦 XHOSA ✅ NEW v1.3.0
|
|
272
|
+
//
|
|
273
|
+
// Xhosa shares Nguni root structure with Zulu. Subject concords differ
|
|
274
|
+
// (ndi- / u- / ba-) but financial roots are near-identical.
|
|
275
|
+
// ────────────────────────────────────────────────
|
|
276
|
+
{ pattern: /thumela?\b/i, capability: 'transfer', lang: 'xh' }, // thumela (send)
|
|
277
|
+
{ pattern: /thumela?\s+imali/i, capability: 'transfer', lang: 'xh' }, // send money
|
|
278
|
+
{ pattern: /hlawul/i, capability: 'payment', lang: 'xh' }, // pay (shared Nguni root)
|
|
279
|
+
{ pattern: /beka\s+(?:imali|ingeniso)/i, capability: 'deposit', lang: 'xh' }, // deposit
|
|
280
|
+
{ pattern: /rhola\s+imali/i, capability: 'withdrawal', lang: 'xh' }, // withdraw money
|
|
281
|
+
{ pattern: /ndi(?:thumele|hlawule|beke|rhola)/i, capability: 'unauthorized_action', lang: 'xh' }, // 1st person perfect
|
|
282
|
+
{ pattern: /u(?:thumele|hlawule|beke|rhola)/i, capability: 'unauthorized_action', lang: 'xh' }, // 3rd person perfect
|
|
283
|
+
|
|
284
|
+
// ────────────────────────────────────────────────
|
|
285
|
+
// 🇬🇭 GHANA: TWI (AKAN) ✅ NEW v1.3.0-alpha
|
|
286
|
+
//
|
|
287
|
+
// Twi is the most widely spoken language in Ghana.
|
|
288
|
+
// Financial roots: 'soma' (send), 'tua' (pay), 'fa' (take/use), 'kɔ' (go)
|
|
289
|
+
// ────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
// Transfer/Send Money
|
|
292
|
+
{ pattern: /soma\s+(?:sika|money)/i, capability: 'transfer', lang: 'tw' }, // Send money
|
|
293
|
+
{ pattern: /de\s+sika\s+ma/i, capability: 'transfer', lang: 'tw' }, // Give money
|
|
294
|
+
{ pattern: /fa\s+sika\s+(?:kɔ|yi)/i, capability: 'transfer', lang: 'tw' }, // Take money away
|
|
295
|
+
|
|
296
|
+
// Payment
|
|
297
|
+
{ pattern: /tua\s+(?:ka|sika)/i, capability: 'payment', lang: 'tw' }, // Pay debt/money
|
|
298
|
+
{ pattern: /san\s+ka\s+sika/i, capability: 'payment', lang: 'tw' }, // Pay back money
|
|
299
|
+
|
|
300
|
+
// Withdrawal
|
|
301
|
+
{ pattern: /yi\s+sika\s+fi/i, capability: 'withdrawal', lang: 'tw' }, // Remove money from...
|
|
302
|
+
{ pattern: /twa\s+sika\s+fi\s+(?:bank|account)/i, capability: 'withdrawal', lang: 'tw' }, // Cut/withdraw money
|
|
303
|
+
|
|
304
|
+
// Deposit
|
|
305
|
+
{ pattern: /de\s+sika\s+to/i, capability: 'deposit', lang: 'tw' }, // Put money in
|
|
306
|
+
{ pattern: /hyeh\s+sika\s+mu/i, capability: 'deposit', lang: 'tw' }, // Insert money into
|
|
307
|
+
|
|
308
|
+
// Unauthorized Action (First Person Perfective - "I have sent/paid")
|
|
309
|
+
{ pattern: /ma(?:te|ye)\s+(?:soma|tua|yi|de)/i, capability: 'unauthorized_action', lang: 'tw' },
|
|
310
|
+
{ pattern: /me(?:soma|tua|yi|de)/i, capability: 'unauthorized_action', lang: 'tw' }, // I send/pay/take
|
|
311
|
+
|
|
312
|
+
// ────────────────────────────────────────────────
|
|
313
|
+
// 🌐 GLOBAL LANGUAGES
|
|
314
|
+
// ────────────────────────────────────────────────
|
|
315
|
+
{ pattern: /\b(transfer(?:red|ring)?|send(?:t|ing)?|wire(?:d)?|pay(?:ed|ing)?|withdraw(?:n)?|deposit(?:ed|ing)?)\b/i, capability: 'financial_action', lang: 'en' },
|
|
316
|
+
{ pattern: /\bI\s+(?:can|will|am able to|have|'ve|did|already)\s+(?:transfer|send|pay|withdraw|deposit|wire)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
317
|
+
{ pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
318
|
+
{ pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
319
|
+
{ pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
|
|
320
|
+
{ pattern: /\b(?:j'?ai|tu as|il a|elle a|nous avons|vous avez|ils ont|elles ont)\s+(?:viré|transféré|envoyé|payé|retiré|déposé)\b/i, capability: 'unauthorized_action', lang: 'fr' },
|
|
321
|
+
{ pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
|
|
322
|
+
{ pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
|
|
323
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|支付|存款|取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
324
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|转帐|支付|付款|提款|取款|存款|存入|汇款|存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
325
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:我|已|已经)\s*(?:转账|支付|提款|存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
|
|
326
|
+
|
|
327
|
+
// ────────────────────────────────────────────────
|
|
328
|
+
// 🛡️ PII & EVASION (kept here for LLM output scanning — not for redaction)
|
|
329
|
+
// ────────────────────────────────────────────────
|
|
330
|
+
{ pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
|
|
331
|
+
{ pattern: /\b(?:bvn|bank\s+verification\s+number)\b.{0,20}\d{11}/i, capability: 'pii_exposure', lang: 'multi' },
|
|
332
|
+
{ pattern: /(?:account|acct|a\/c|akaunti|asusu|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})/i, capability: 'pii_exposure', lang: 'multi' },
|
|
333
|
+
{ pattern: /\b(successful(?:ly)?|confirmed|approved|completed|processed|verified|imethibitishwa|imefanikiwa)\b/i, capability: 'deceptive_claim', lang: 'multi' },
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ================================
|
|
338
|
+
// ✅ NEW v1.3.0 — PII REDACTION ENGINE
|
|
339
|
+
//
|
|
340
|
+
// Replaces PII tokens in a string with [LABEL_REDACTED] placeholders.
|
|
341
|
+
// Returns the cleaned string + a structured redaction manifest for audit.
|
|
342
|
+
//
|
|
343
|
+
// This is intentionally separate from _validateInputs so callers can
|
|
344
|
+
// choose to redact without halting (OLANG_PII_MODE=redact).
|
|
345
|
+
// ================================
|
|
346
|
+
|
|
347
|
+
_redactPII(text) {
|
|
348
|
+
if (!text || typeof text !== 'string') {
|
|
349
|
+
return { redacted: text, redactions: [], wasModified: false };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let redacted = text;
|
|
353
|
+
const redactions = [];
|
|
354
|
+
|
|
355
|
+
for (const { pattern, capability, lang, label } of this._getPIIPatterns()) {
|
|
356
|
+
// All PII patterns must use /g flag for replaceAll behaviour
|
|
357
|
+
const globalPattern = pattern.global
|
|
358
|
+
? pattern
|
|
359
|
+
: new RegExp(pattern.source, pattern.flags + 'g');
|
|
360
|
+
|
|
361
|
+
redacted = redacted.replace(globalPattern, (match) => {
|
|
362
|
+
redactions.push({
|
|
363
|
+
original: match,
|
|
364
|
+
replacement: `[${label}_REDACTED]`,
|
|
365
|
+
capability,
|
|
366
|
+
lang,
|
|
367
|
+
offset: redacted.indexOf(match) // approximate; accurate before mutations
|
|
368
|
+
});
|
|
369
|
+
return `[${label}_REDACTED]`;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
redacted,
|
|
375
|
+
redactions,
|
|
376
|
+
wasModified: redactions.length > 0
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ================================
|
|
381
|
+
// ✅ NEW v1.3.0 — PUBLIC REDACTION API
|
|
382
|
+
//
|
|
383
|
+
// Allows workflow authors and external callers to redact a string directly.
|
|
384
|
+
// Backward compat: the old _validateInputs path is unchanged.
|
|
385
|
+
//
|
|
386
|
+
// const { redacted, redactions } = runtime.redact(text);
|
|
387
|
+
// ================================
|
|
388
|
+
|
|
389
|
+
redact(text) {
|
|
390
|
+
return this._redactPII(text);
|
|
38
391
|
}
|
|
39
392
|
|
|
393
|
+
// ================================
|
|
40
394
|
// ✅ NEW: Initialize database client
|
|
395
|
+
// ================================
|
|
41
396
|
_initDbClient() {
|
|
42
397
|
const dbType = process.env.OLANG_DB_TYPE; // 'postgres', 'mysql', 'mongodb', 'sqlite'
|
|
43
398
|
if (!dbType) return; // DB persistence disabled
|
|
@@ -141,33 +496,33 @@ class RuntimeAPI {
|
|
|
141
496
|
previousHash: this.previousHash,
|
|
142
497
|
sequenceNumber: this.auditLog.length + 1
|
|
143
498
|
};
|
|
144
|
-
|
|
499
|
+
|
|
145
500
|
// Create hash of this entry
|
|
146
501
|
const entryHash = this._hash(entryData);
|
|
147
|
-
|
|
502
|
+
|
|
148
503
|
// Sign the entry if private key available
|
|
149
504
|
const signature = this._sign(entryHash);
|
|
150
|
-
|
|
505
|
+
|
|
151
506
|
const entry = {
|
|
152
507
|
...entryData,
|
|
153
508
|
hash: entryHash,
|
|
154
509
|
signature,
|
|
155
|
-
publicKey: this.auditLogPrivateKey ?
|
|
156
|
-
crypto.createPublicKey(this.auditLogPrivateKey).export({
|
|
157
|
-
type: 'spki',
|
|
158
|
-
format: 'pem'
|
|
510
|
+
publicKey: this.auditLogPrivateKey ?
|
|
511
|
+
crypto.createPublicKey(this.auditLogPrivateKey).export({
|
|
512
|
+
type: 'spki',
|
|
513
|
+
format: 'pem'
|
|
159
514
|
}) : null
|
|
160
515
|
};
|
|
161
|
-
|
|
516
|
+
|
|
162
517
|
// Update chain
|
|
163
518
|
this.previousHash = entryHash;
|
|
164
519
|
this.auditLog.push(entry);
|
|
165
|
-
|
|
520
|
+
|
|
166
521
|
// Persist to file if enabled
|
|
167
522
|
if (this.enableAuditLog) {
|
|
168
523
|
this._persistAuditLog();
|
|
169
524
|
}
|
|
170
|
-
|
|
525
|
+
|
|
171
526
|
return entry;
|
|
172
527
|
}
|
|
173
528
|
|
|
@@ -181,13 +536,13 @@ class RuntimeAPI {
|
|
|
181
536
|
'current_step',
|
|
182
537
|
'agent_id'
|
|
183
538
|
];
|
|
184
|
-
|
|
539
|
+
|
|
185
540
|
for (const key of keysToCapture) {
|
|
186
541
|
if (this.context[key] !== undefined) {
|
|
187
542
|
snapshot[key] = this.context[key];
|
|
188
543
|
}
|
|
189
544
|
}
|
|
190
|
-
|
|
545
|
+
|
|
191
546
|
return snapshot;
|
|
192
547
|
}
|
|
193
548
|
|
|
@@ -201,7 +556,7 @@ class RuntimeAPI {
|
|
|
201
556
|
JSON.stringify(this.auditLog, null, 2),
|
|
202
557
|
'utf8'
|
|
203
558
|
);
|
|
204
|
-
|
|
559
|
+
|
|
205
560
|
// Also persist to DB if configured
|
|
206
561
|
if (this.dbClient && process.env.OLANG_AUDIT_DB_PERSIST === 'true') {
|
|
207
562
|
this._persistAuditLogToDB();
|
|
@@ -218,7 +573,7 @@ class RuntimeAPI {
|
|
|
218
573
|
try {
|
|
219
574
|
const latestEntry = this.auditLog[this.auditLog.length - 1];
|
|
220
575
|
if (!latestEntry) return;
|
|
221
|
-
|
|
576
|
+
|
|
222
577
|
switch (this.dbClient.type) {
|
|
223
578
|
case 'postgres':
|
|
224
579
|
await this.dbClient.client.query(
|
|
@@ -236,7 +591,7 @@ class RuntimeAPI {
|
|
|
236
591
|
]
|
|
237
592
|
);
|
|
238
593
|
break;
|
|
239
|
-
|
|
594
|
+
|
|
240
595
|
case 'mysql':
|
|
241
596
|
await this.dbClient.client.execute(
|
|
242
597
|
`INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
|
|
@@ -253,7 +608,7 @@ class RuntimeAPI {
|
|
|
253
608
|
]
|
|
254
609
|
);
|
|
255
610
|
break;
|
|
256
|
-
|
|
611
|
+
|
|
257
612
|
case 'mongodb':
|
|
258
613
|
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
259
614
|
await db.collection('audit_log').insertOne({
|
|
@@ -267,7 +622,7 @@ class RuntimeAPI {
|
|
|
267
622
|
sequence_number: latestEntry.sequenceNumber
|
|
268
623
|
});
|
|
269
624
|
break;
|
|
270
|
-
|
|
625
|
+
|
|
271
626
|
case 'sqlite':
|
|
272
627
|
const stmt = this.dbClient.client.prepare(
|
|
273
628
|
`INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
|
|
@@ -298,28 +653,28 @@ class RuntimeAPI {
|
|
|
298
653
|
if (log.length === 0) {
|
|
299
654
|
return { valid: true, message: 'Audit log is empty' };
|
|
300
655
|
}
|
|
301
|
-
|
|
656
|
+
|
|
302
657
|
// Verify genesis block
|
|
303
658
|
if (log[0].previousHash !== 'GENESIS') {
|
|
304
659
|
return { valid: false, error: 'Invalid genesis block', failedAtIndex: 0 };
|
|
305
660
|
}
|
|
306
|
-
|
|
661
|
+
|
|
307
662
|
// Verify chain integrity
|
|
308
663
|
let previousHash = 'GENESIS';
|
|
309
664
|
for (let i = 0; i < log.length; i++) {
|
|
310
665
|
const entry = log[i];
|
|
311
|
-
|
|
666
|
+
|
|
312
667
|
// Check previous hash linkage
|
|
313
668
|
if (entry.previousHash !== previousHash) {
|
|
314
|
-
return {
|
|
315
|
-
valid: false,
|
|
669
|
+
return {
|
|
670
|
+
valid: false,
|
|
316
671
|
error: `Hash chain broken at entry ${i}`,
|
|
317
672
|
failedAtIndex: i,
|
|
318
673
|
expected: previousHash,
|
|
319
674
|
actual: entry.previousHash
|
|
320
675
|
};
|
|
321
676
|
}
|
|
322
|
-
|
|
677
|
+
|
|
323
678
|
// Verify entry hash
|
|
324
679
|
const entryData = {
|
|
325
680
|
timestamp: entry.timestamp,
|
|
@@ -330,18 +685,18 @@ class RuntimeAPI {
|
|
|
330
685
|
previousHash: entry.previousHash,
|
|
331
686
|
sequenceNumber: entry.sequenceNumber
|
|
332
687
|
};
|
|
333
|
-
|
|
688
|
+
|
|
334
689
|
const calculatedHash = this._hash(entryData);
|
|
335
690
|
if (calculatedHash !== entry.hash) {
|
|
336
|
-
return {
|
|
337
|
-
valid: false,
|
|
691
|
+
return {
|
|
692
|
+
valid: false,
|
|
338
693
|
error: `Entry hash mismatch at index ${i}`,
|
|
339
694
|
failedAtIndex: i,
|
|
340
695
|
expected: calculatedHash,
|
|
341
696
|
actual: entry.hash
|
|
342
697
|
};
|
|
343
698
|
}
|
|
344
|
-
|
|
699
|
+
|
|
345
700
|
// Verify signature if present
|
|
346
701
|
if (entry.signature && entry.publicKey) {
|
|
347
702
|
try {
|
|
@@ -350,26 +705,26 @@ class RuntimeAPI {
|
|
|
350
705
|
verify.end();
|
|
351
706
|
const isValid = verify.verify(entry.publicKey, entry.signature, 'base64');
|
|
352
707
|
if (!isValid) {
|
|
353
|
-
return {
|
|
354
|
-
valid: false,
|
|
708
|
+
return {
|
|
709
|
+
valid: false,
|
|
355
710
|
error: `Signature verification failed at entry ${i}`,
|
|
356
711
|
failedAtIndex: i
|
|
357
712
|
};
|
|
358
713
|
}
|
|
359
714
|
} catch (e) {
|
|
360
|
-
return {
|
|
361
|
-
valid: false,
|
|
715
|
+
return {
|
|
716
|
+
valid: false,
|
|
362
717
|
error: `Signature verification error at entry ${i}: ${e.message}`,
|
|
363
718
|
failedAtIndex: i
|
|
364
719
|
};
|
|
365
720
|
}
|
|
366
721
|
}
|
|
367
|
-
|
|
722
|
+
|
|
368
723
|
previousHash = entry.hash;
|
|
369
724
|
}
|
|
370
|
-
|
|
371
|
-
return {
|
|
372
|
-
valid: true,
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
valid: true,
|
|
373
728
|
message: `Audit log verified successfully (${log.length} entries)`,
|
|
374
729
|
totalEntries: log.length,
|
|
375
730
|
lastHash: previousHash
|
|
@@ -387,7 +742,7 @@ class RuntimeAPI {
|
|
|
387
742
|
nextHash: endIndex < this.auditLog.length - 1 ? this.auditLog[endIndex + 1].hash : null,
|
|
388
743
|
totalEntries: this.auditLog.length
|
|
389
744
|
};
|
|
390
|
-
|
|
745
|
+
|
|
391
746
|
return proof;
|
|
392
747
|
}
|
|
393
748
|
|
|
@@ -410,9 +765,9 @@ class RuntimeAPI {
|
|
|
410
765
|
*/
|
|
411
766
|
_calculateMerkleRoot() {
|
|
412
767
|
if (this.auditLog.length === 0) return null;
|
|
413
|
-
|
|
768
|
+
|
|
414
769
|
let hashes = this.auditLog.map(entry => entry.hash);
|
|
415
|
-
|
|
770
|
+
|
|
416
771
|
while (hashes.length > 1) {
|
|
417
772
|
const newLevel = [];
|
|
418
773
|
for (let i = 0; i < hashes.length; i += 2) {
|
|
@@ -422,7 +777,7 @@ class RuntimeAPI {
|
|
|
422
777
|
}
|
|
423
778
|
hashes = newLevel;
|
|
424
779
|
}
|
|
425
|
-
|
|
780
|
+
|
|
426
781
|
return hashes[0];
|
|
427
782
|
}
|
|
428
783
|
|
|
@@ -458,7 +813,7 @@ class RuntimeAPI {
|
|
|
458
813
|
resolverPolicy: 'allowlist-only',
|
|
459
814
|
timestamp: new Date().toISOString()
|
|
460
815
|
};
|
|
461
|
-
|
|
816
|
+
|
|
462
817
|
return crypto.createHash('sha256')
|
|
463
818
|
.update(JSON.stringify(profile))
|
|
464
819
|
.digest('hex');
|
|
@@ -475,12 +830,16 @@ class RuntimeAPI {
|
|
|
475
830
|
semanticValidation: true,
|
|
476
831
|
hallucinationPrevention: true,
|
|
477
832
|
cryptographicAudit: true,
|
|
478
|
-
multiDatabaseSupport: true
|
|
833
|
+
multiDatabaseSupport: true,
|
|
834
|
+
piiRedaction: true, // ✅ NEW v1.3.0
|
|
835
|
+
piiMode: this.piiMode, // ✅ NEW v1.3.0
|
|
836
|
+
xhosaSupport: true // ✅ NEW v1.3.0
|
|
479
837
|
},
|
|
480
838
|
environment: {
|
|
481
839
|
auditEnabled: this.enableAuditLog,
|
|
482
840
|
dbType: this.dbClient?.type || 'none',
|
|
483
|
-
strictMode: process.env.OLANG_STRICT_INPUTS === 'true'
|
|
841
|
+
strictMode: process.env.OLANG_STRICT_INPUTS === 'true',
|
|
842
|
+
piiMode: this.piiMode // ✅ NEW v1.3.0
|
|
484
843
|
}
|
|
485
844
|
};
|
|
486
845
|
}
|
|
@@ -543,7 +902,7 @@ class RuntimeAPI {
|
|
|
543
902
|
const entry = { resolver: resolverName, step: stepAction, timestamp: new Date().toISOString() };
|
|
544
903
|
fs.appendFileSync(this.disallowedLogFile, JSON.stringify(entry) + '\n', 'utf8');
|
|
545
904
|
this.disallowedAttempts.push(entry);
|
|
546
|
-
|
|
905
|
+
|
|
547
906
|
// ✅ AUDIT LOG: Security violation with governance context
|
|
548
907
|
this._createAuditEntry('security_violation', {
|
|
549
908
|
type: 'disallowed_resolver',
|
|
@@ -551,12 +910,12 @@ class RuntimeAPI {
|
|
|
551
910
|
step: stepAction,
|
|
552
911
|
severity: 'high',
|
|
553
912
|
kernel_version: KERNEL_VERSION,
|
|
554
|
-
governance_profile_hash: this._generateGovernanceProfileHash({
|
|
913
|
+
governance_profile_hash: this._generateGovernanceProfileHash({
|
|
555
914
|
allowedResolvers: Array.from(this.allowedResolvers),
|
|
556
|
-
maxGenerations: null
|
|
915
|
+
maxGenerations: null
|
|
557
916
|
})
|
|
558
917
|
});
|
|
559
|
-
|
|
918
|
+
|
|
560
919
|
if (this.verbose) {
|
|
561
920
|
console.warn(`[O-Lang] Disallowed resolver blocked: ${resolverName} | step: ${stepAction}`);
|
|
562
921
|
}
|
|
@@ -638,38 +997,131 @@ class RuntimeAPI {
|
|
|
638
997
|
return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
|
|
639
998
|
}
|
|
640
999
|
|
|
641
|
-
|
|
1000
|
+
/**
|
|
1001
|
+
* evaluateCondition(cond, ctx)
|
|
1002
|
+
*
|
|
1003
|
+
* Governance Features:
|
|
1004
|
+
* 1. Quote-Aware Parsing: Prevents splitting on "or"/"and" inside quoted strings.
|
|
1005
|
+
* 2. Strict Equality: Uses === to prevent type-coercion safety bypasses.
|
|
1006
|
+
* 3. Comprehensive Operators: Supports gte, lte, contains, not equals.
|
|
1007
|
+
* 4. Auditability: Warns on unrecognised syntax to prevent silent failures.
|
|
1008
|
+
*/
|
|
1009
|
+
evaluateCondition(cond, ctx) {
|
|
1010
|
+
if (!cond) return false;
|
|
642
1011
|
cond = cond.trim();
|
|
643
1012
|
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
1013
|
+
// ── Helper: split on logical operators OUTSIDE quoted strings ────────────
|
|
1014
|
+
const splitOutsideQuotes = (str, regex) => {
|
|
1015
|
+
const parts = [];
|
|
1016
|
+
let current = '';
|
|
1017
|
+
let inQuote = false;
|
|
1018
|
+
let quoteChar = '';
|
|
1019
|
+
|
|
1020
|
+
for (let i = 0; i < str.length; i++) {
|
|
1021
|
+
const ch = str[i];
|
|
1022
|
+
|
|
1023
|
+
// Handle quote toggling
|
|
1024
|
+
if (!inQuote && (ch === '"' || ch === "'")) {
|
|
1025
|
+
inQuote = true;
|
|
1026
|
+
quoteChar = ch;
|
|
1027
|
+
current += ch;
|
|
1028
|
+
} else if (inQuote && ch === quoteChar) {
|
|
1029
|
+
// Check for escaped quote? For now, simple toggle.
|
|
1030
|
+
inQuote = false;
|
|
1031
|
+
quoteChar = '';
|
|
1032
|
+
current += ch;
|
|
1033
|
+
} else if (!inQuote) {
|
|
1034
|
+
// Check for operator match at current position
|
|
1035
|
+
const remaining = str.slice(i);
|
|
1036
|
+
const m = remaining.match(regex);
|
|
1037
|
+
if (m && m.index === 0) {
|
|
1038
|
+
parts.push(current);
|
|
1039
|
+
current = '';
|
|
1040
|
+
i += m[0].length - 1;
|
|
1041
|
+
continue;
|
|
1042
|
+
} else {
|
|
1043
|
+
current += ch;
|
|
1044
|
+
}
|
|
1045
|
+
} else {
|
|
1046
|
+
current += ch;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
parts.push(current);
|
|
1050
|
+
return parts.map(p => p.trim()).filter(Boolean);
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
// ── 1. Logical OR ─────────────────────────────────────────────────────────
|
|
1054
|
+
// (?!\s+equal) prevents splitting on "or" in "greater than or equal"
|
|
1055
|
+
const orParts = splitOutsideQuotes(cond, /^(\|\||\bor\b(?!\s+equal))/i);
|
|
1056
|
+
if (orParts.length > 1) {
|
|
1057
|
+
return orParts.some(c => this.evaluateCondition(c.trim(), ctx));
|
|
647
1058
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1059
|
+
|
|
1060
|
+
// ── 2. Logical AND ────────────────────────────────────────────────────────
|
|
1061
|
+
const andParts = splitOutsideQuotes(cond, /^(&&|\band\b)/i);
|
|
1062
|
+
if (andParts.length > 1) {
|
|
1063
|
+
return andParts.every(c => this.evaluateCondition(c.trim(), ctx));
|
|
651
1064
|
}
|
|
652
1065
|
|
|
653
|
-
//
|
|
654
|
-
const eqMatch = cond.match(/^(?:\{(
|
|
1066
|
+
// ── 3. Strict equality: {var} === "value" or {var} == "value" ────────────
|
|
1067
|
+
const eqMatch = cond.match(/^(?:\{(.+?)\}|(\w[\w.]*?))\s*===?\s*"(.*)"$/);
|
|
655
1068
|
if (eqMatch) {
|
|
656
1069
|
const key = eqMatch[1] || eqMatch[2];
|
|
657
1070
|
return this.getNested(ctx, key) === eqMatch[3];
|
|
658
1071
|
}
|
|
659
1072
|
|
|
660
|
-
//
|
|
661
|
-
const
|
|
662
|
-
if (
|
|
1073
|
+
// ── 4. Not equals: {var} != "value" or {var} !== "value" ─────────────────
|
|
1074
|
+
const neqMatch = cond.match(/^(?:\{(.+?)\}|(\w[\w.]*?))\s*!==?\s*"(.*)"$/);
|
|
1075
|
+
if (neqMatch) {
|
|
1076
|
+
const key = neqMatch[1] || neqMatch[2];
|
|
1077
|
+
return this.getNested(ctx, key) !== neqMatch[3];
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// ── 5. O-Lang keyword: {var} equals "value" (strict) ─────────────────────
|
|
1081
|
+
const oldEq = cond.match(/^\{(.+?)\}\s+equals\s+"(.*)"$/);
|
|
1082
|
+
if (oldEq) return this.getNested(ctx, oldEq[1]) === oldEq[2];
|
|
1083
|
+
|
|
1084
|
+
// ── 6. O-Lang keyword: {var} not equals "value" ───────────────────────────
|
|
1085
|
+
const notEq = cond.match(/^\{(.+?)\}\s+not\s+equals\s+"(.*)"$/);
|
|
1086
|
+
if (notEq) return this.getNested(ctx, notEq[1]) !== notEq[2];
|
|
1087
|
+
|
|
1088
|
+
// ── 7. Contains: {var} contains "value" ──────────────────────────────────
|
|
1089
|
+
const containsMatch = cond.match(/^\{(.+?)\}\s+contains\s+"(.*)"$/);
|
|
1090
|
+
if (containsMatch) {
|
|
1091
|
+
const value = this.getNested(ctx, containsMatch[1]);
|
|
1092
|
+
const target = containsMatch[2];
|
|
1093
|
+
if (Array.isArray(value)) return value.includes(target);
|
|
1094
|
+
if (typeof value === 'string') return value.includes(target);
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// ── 8. Numeric comparisons (GTE/LTE before GT/LT) ────────────────────────
|
|
1099
|
+
const gte = cond.match(/^\{(.+?)\}\s+greater than or equal\s+(\d+\.?\d*)$/);
|
|
1100
|
+
if (gte) return parseFloat(this.getNested(ctx, gte[1])) >= parseFloat(gte[2]);
|
|
663
1101
|
|
|
664
|
-
const
|
|
1102
|
+
const lte = cond.match(/^\{(.+?)\}\s+less than or equal\s+(\d+\.?\d*)$/);
|
|
1103
|
+
if (lte) return parseFloat(this.getNested(ctx, lte[1])) <= parseFloat(lte[2]);
|
|
1104
|
+
|
|
1105
|
+
const gt = cond.match(/^\{(.+?)\}\s+greater than\s+(\d+\.?\d*)$/);
|
|
665
1106
|
if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
|
|
666
1107
|
|
|
667
|
-
const lt = cond.match(/^\{(
|
|
1108
|
+
const lt = cond.match(/^\{(.+?)\}\s+less than\s+(\d+\.?\d*)$/);
|
|
668
1109
|
if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
|
|
669
1110
|
|
|
670
|
-
//
|
|
671
|
-
|
|
1111
|
+
// ── 9. Truthy fallback — warn so authors know it fired ───────────────────
|
|
1112
|
+
const fallbackKey = cond.replace(/^\{|\}$/g, '');
|
|
1113
|
+
const fallbackValue = this.getNested(ctx, fallbackKey);
|
|
1114
|
+
|
|
1115
|
+
this.addWarning(
|
|
1116
|
+
`evaluateCondition: unrecognised condition syntax "${cond}" — ` +
|
|
1117
|
+
`falling back to truthy check on "${fallbackKey}" ` +
|
|
1118
|
+
`(value: ${JSON.stringify(fallbackValue)}). ` +
|
|
1119
|
+
`If this is unintentional, check your condition syntax.`
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
return Boolean(fallbackValue);
|
|
672
1123
|
}
|
|
1124
|
+
|
|
673
1125
|
mathFunctions = {
|
|
674
1126
|
add: (a, b) => a + b,
|
|
675
1127
|
subtract: (a, b) => a - b,
|
|
@@ -690,38 +1142,38 @@ class RuntimeAPI {
|
|
|
690
1142
|
abs: a => Math.abs(a)
|
|
691
1143
|
};
|
|
692
1144
|
|
|
693
|
-
|
|
1145
|
+
evaluateMath(expr) {
|
|
694
1146
|
// ✅ Handle quoted string literals with interpolation: "{var}" → interpolated string
|
|
695
1147
|
if (typeof expr === 'string') {
|
|
696
1148
|
const trimmed = expr.trim();
|
|
697
|
-
|
|
1149
|
+
|
|
698
1150
|
// Check if it's a quoted string (single or double quotes)
|
|
699
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
1151
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
700
1152
|
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
701
1153
|
// Extract the inner content
|
|
702
1154
|
let inner = trimmed.slice(1, -1);
|
|
703
|
-
|
|
1155
|
+
|
|
704
1156
|
// Perform interpolation: replace {var} with context values
|
|
705
1157
|
inner = inner.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
706
1158
|
const value = this.getNested(this.context, path.trim());
|
|
707
1159
|
return value !== undefined ? String(value) : `{${path}}`;
|
|
708
1160
|
});
|
|
709
|
-
|
|
1161
|
+
|
|
710
1162
|
return inner;
|
|
711
1163
|
}
|
|
712
1164
|
}
|
|
713
|
-
|
|
1165
|
+
|
|
714
1166
|
// ── Original math evaluation logic (unchanged) ──────────────────────────
|
|
715
1167
|
expr = expr.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
716
1168
|
const value = this.getNested(this.context, path.trim());
|
|
717
1169
|
if (typeof value === 'string') return `"${value.replace(/"/g, '\\"')}"`;
|
|
718
1170
|
return value !== undefined ? value : 0;
|
|
719
1171
|
});
|
|
720
|
-
|
|
1172
|
+
|
|
721
1173
|
const funcNames = Object.keys(this.mathFunctions);
|
|
722
1174
|
const safeFunc = {};
|
|
723
1175
|
funcNames.forEach(fn => safeFunc[fn] = this.mathFunctions[fn]);
|
|
724
|
-
|
|
1176
|
+
|
|
725
1177
|
try {
|
|
726
1178
|
const f = new Function(...funcNames, `return ${expr};`);
|
|
727
1179
|
return f(...funcNames.map(fn => safeFunc[fn]));
|
|
@@ -765,350 +1217,264 @@ class RuntimeAPI {
|
|
|
765
1217
|
});
|
|
766
1218
|
}
|
|
767
1219
|
|
|
768
|
-
//
|
|
769
|
-
// ✅ KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety)
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
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
|
-
|
|
1220
|
+
// ================================
|
|
1221
|
+
// ✅ UPDATED v1.3.0 — KERNEL-LEVEL INPUT VALIDATION (Pre-Flight Safety)
|
|
1222
|
+
//
|
|
1223
|
+
// BACKWARD COMPAT:
|
|
1224
|
+
// OLANG_PII_MODE=block (default) → original behaviour, throws on any match
|
|
1225
|
+
// OLANG_PII_MODE=redact → replaces PII tokens, continues
|
|
1226
|
+
// OLANG_PII_MODE=redact-and-log → redact + audit entry per redaction event
|
|
1227
|
+
//
|
|
1228
|
+
// Financial intent patterns always throw regardless of PII mode.
|
|
1229
|
+
// ================================
|
|
1230
|
+
_validateInputs(inputs) {
|
|
1231
|
+
// Only scan specific input fields that contain user text
|
|
1232
|
+
// NOTE: 'document_text' intentionally excluded — legal documents legitimately
|
|
1233
|
+
// contain financial terms and must not be blocked at the input layer.
|
|
1234
|
+
const fieldsToScan = ['user_message', 'user_question', 'text', 'prompt'];
|
|
1235
|
+
|
|
1236
|
+
const redactMode = this.piiMode === 'redact' || this.piiMode === 'redact-and-log';
|
|
1237
|
+
const allRedactions = {};
|
|
1238
|
+
|
|
1239
|
+
// ── PASS 1: PII redaction (when mode allows) ──────────────────────────
|
|
1240
|
+
if (redactMode) {
|
|
1241
|
+
for (const field of fieldsToScan) {
|
|
1242
|
+
const text = inputs[field];
|
|
1243
|
+
if (!text || typeof text !== 'string') continue;
|
|
1244
|
+
|
|
1245
|
+
const { redacted, redactions, wasModified } = this._redactPII(text);
|
|
1246
|
+
|
|
1247
|
+
if (wasModified) {
|
|
1248
|
+
inputs[field] = redacted; // mutate in place — caller sees clean value
|
|
1249
|
+
allRedactions[field] = redactions;
|
|
1250
|
+
|
|
1251
|
+
if (this.piiMode === 'redact-and-log') {
|
|
1252
|
+
this._createAuditEntry('pii_redacted', {
|
|
1253
|
+
field,
|
|
1254
|
+
redaction_count: redactions.length,
|
|
1255
|
+
redactions: redactions.map(r => ({
|
|
1256
|
+
label: r.replacement,
|
|
1257
|
+
capability: r.capability,
|
|
1258
|
+
lang: r.lang
|
|
1259
|
+
// NOTE: original value intentionally excluded from audit log
|
|
1260
|
+
// to avoid persisting the very PII we just redacted
|
|
1261
|
+
})),
|
|
1262
|
+
severity: 'info'
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
811
1265
|
|
|
1266
|
+
if (this.verbose) {
|
|
1267
|
+
console.log(
|
|
1268
|
+
`🛡️ [O-Lang PII] Redacted ${redactions.length} item(s) in "${field}": ` +
|
|
1269
|
+
redactions.map(r => r.replacement).join(', ')
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
812
1275
|
|
|
813
|
-
|
|
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' },
|
|
1276
|
+
// ── PASS 2: Financial intent scan (always blocks, mode-independent) ───
|
|
1277
|
+
for (const field of fieldsToScan) {
|
|
1278
|
+
const text = inputs[field]; // may already be PII-redacted from pass 1
|
|
1279
|
+
if (!text || typeof text !== 'string') continue;
|
|
820
1280
|
|
|
821
|
-
//
|
|
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' },
|
|
1281
|
+
// 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION (INPUT)
|
|
1282
|
+
for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
|
|
1283
|
+
if (pattern.test(text)) {
|
|
1284
|
+
const match = text.match(pattern);
|
|
1285
|
+
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
|
|
1286
|
+
const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
|
|
830
1287
|
|
|
1288
|
+
// ✅ DECOUPLED: Check legal context via standardized signals (not UI fields)
|
|
1289
|
+
const intent = this.context.__verified_intent || {};
|
|
1290
|
+
const signals = intent.context_signals || {};
|
|
1291
|
+
|
|
1292
|
+
const isLegalContext =
|
|
1293
|
+
// Signal 1: Explicit scope declaration
|
|
1294
|
+
intent.scope === 'legal_analysis_only' ||
|
|
1295
|
+
|
|
1296
|
+
// Signal 2: Standardized context signals (server-mapped, UI-agnostic)
|
|
1297
|
+
signals.isLegalDocument === true ||
|
|
1298
|
+
signals.documentCategory === 'contract' ||
|
|
1299
|
+
signals.documentCategory === 'nda' ||
|
|
1300
|
+
signals.documentCategory === 'agreement' ||
|
|
1301
|
+
signals.documentCategory === 'legal' ||
|
|
1302
|
+
|
|
1303
|
+
// Signal 3: Semantic fallback (works even if signals missing)
|
|
1304
|
+
(typeof text === 'string' && /clause|term|agreement|contract|obligation|penalty|damages|breach|party|shall|herein/i.test(text));
|
|
1305
|
+
|
|
1306
|
+
// ✅ NEW: Check contextual allowlist if in legal context
|
|
1307
|
+
if (isLegalContext && this.context.__verified_intent?.contextual_allowlist) {
|
|
1308
|
+
const allowlist = this.context.__verified_intent.contextual_allowlist;
|
|
1309
|
+
const triggerWord = match ? match[0].toLowerCase() : '';
|
|
1310
|
+
|
|
1311
|
+
const allowed = allowlist.some(rule => {
|
|
1312
|
+
// Check if this pattern's capability matches the rule's trigger
|
|
1313
|
+
const triggerMatch =
|
|
1314
|
+
triggerWord.includes(rule.trigger.toLowerCase()) ||
|
|
1315
|
+
capability.toLowerCase().includes(rule.trigger.toLowerCase()); // ← Handle capability-level triggers
|
|
1316
|
+
|
|
1317
|
+
if (triggerMatch) {
|
|
1318
|
+
// Check if required legal keywords are present
|
|
1319
|
+
return rule.requires.some(keyword =>
|
|
1320
|
+
text.toLowerCase().includes(keyword.toLowerCase())
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
return false;
|
|
1324
|
+
});
|
|
831
1325
|
|
|
832
|
-
|
|
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' },
|
|
1326
|
+
if (allowed) {
|
|
1327
|
+
// ✅ AUDIT LOG: Contextual allowlist bypass
|
|
1328
|
+
this._createAuditEntry('safety_bypass', {
|
|
1329
|
+
type: 'contextual_allowlist',
|
|
1330
|
+
trigger: triggerWord,
|
|
1331
|
+
legal_context: true,
|
|
1332
|
+
matched_keywords: this.context.__verified_intent.contextual_allowlist
|
|
1333
|
+
.find(r => triggerWord.includes(r.trigger.toLowerCase()))?.requires || [],
|
|
1334
|
+
severity: 'info'
|
|
1335
|
+
});
|
|
1336
|
+
continue; // Skip blocking this match
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
848
1339
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1340
|
+
// ✅ AUDIT LOG: Input Safety Violation (only if not bypassed)
|
|
1341
|
+
this._createAuditEntry('input_safety_violation', {
|
|
1342
|
+
type: 'blocked_input',
|
|
1343
|
+
field: field,
|
|
1344
|
+
detected_phrase: match ? match[0].trim() : 'unknown pattern',
|
|
1345
|
+
capability: capability,
|
|
1346
|
+
language: lang,
|
|
1347
|
+
african_language_detected: isAfrican,
|
|
1348
|
+
financial_expression_found: isFinancial,
|
|
1349
|
+
legal_context_detected: false,
|
|
1350
|
+
severity: 'high'
|
|
1351
|
+
});
|
|
857
1352
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
`[O-Lang SAFETY] Blocked Input in "${lang}":\n` +
|
|
1355
|
+
` → Detected: "${match ? match[0].trim() : 'Pattern Match'}"\n` +
|
|
1356
|
+
` → Capability: ${capability}\n` +
|
|
1357
|
+
` → Field: ${field}\n` +
|
|
1358
|
+
` → African Language Detected: ${isAfrican}\n` +
|
|
1359
|
+
` → Financial Expression: ${isFinancial}\n` +
|
|
1360
|
+
`\n🛑 Workflow halted before execution.`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
866
1365
|
|
|
867
|
-
|
|
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'
|
|
1366
|
+
// ── PASS 3: PII block scan (only in block mode — original behaviour) ──
|
|
1367
|
+
if (!redactMode) {
|
|
1368
|
+
for (const field of fieldsToScan) {
|
|
1369
|
+
const text = inputs[field];
|
|
1370
|
+
if (!text || typeof text !== 'string') continue;
|
|
1371
|
+
|
|
1372
|
+
for (const { pattern, capability, lang, label } of this._getPIIPatterns()) {
|
|
1373
|
+
if (pattern.test(text)) {
|
|
1374
|
+
const match = text.match(pattern);
|
|
1375
|
+
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
|
|
1376
|
+
|
|
1377
|
+
this._createAuditEntry('input_safety_violation', {
|
|
1378
|
+
type: 'blocked_input',
|
|
1379
|
+
field: field,
|
|
1380
|
+
detected_phrase: match ? match[0].trim() : 'unknown pattern',
|
|
1381
|
+
capability: capability,
|
|
1382
|
+
language: lang,
|
|
1383
|
+
african_language_detected: isAfrican,
|
|
1384
|
+
financial_expression_found: false,
|
|
1385
|
+
pii_type: label,
|
|
1386
|
+
severity: 'high'
|
|
921
1387
|
});
|
|
922
|
-
|
|
1388
|
+
|
|
1389
|
+
throw new Error(
|
|
1390
|
+
`[O-Lang SAFETY] Blocked PII in field "${field}" (${lang}):\n` +
|
|
1391
|
+
` → Type: ${label}\n` +
|
|
1392
|
+
` → Capability: ${capability}\n` +
|
|
1393
|
+
`\n🛑 Workflow halted before execution. Set OLANG_PII_MODE=redact to auto-redact instead.`
|
|
1394
|
+
);
|
|
923
1395
|
}
|
|
924
1396
|
}
|
|
925
|
-
|
|
926
|
-
// ✅ AUDIT LOG: Input Safety Violation (only if not bypassed)
|
|
927
|
-
this._createAuditEntry('input_safety_violation', {
|
|
928
|
-
type: 'blocked_input',
|
|
929
|
-
field: field,
|
|
930
|
-
detected_phrase: match ? match[0].trim() : 'unknown pattern',
|
|
931
|
-
capability: capability,
|
|
932
|
-
language: lang,
|
|
933
|
-
african_language_detected: isAfrican,
|
|
934
|
-
financial_expression_found: isFinancial,
|
|
935
|
-
legal_context_detected: isLegalContext,
|
|
936
|
-
severity: 'high'
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
throw new Error(
|
|
940
|
-
`[O-Lang SAFETY] Blocked Input in "${lang}":\n` +
|
|
941
|
-
` → Detected: "${match ? match[0].trim() : 'Pattern Match'}"\n` +
|
|
942
|
-
` → Capability: ${capability}\n` +
|
|
943
|
-
` → Field: ${field}\n` +
|
|
944
|
-
` → African Language Detected: ${isAfrican}\n` +
|
|
945
|
-
` → Financial Expression: ${isFinancial}\n` +
|
|
946
|
-
` → Legal Context: ${isLegalContext}\n` +
|
|
947
|
-
`\n🛑 Workflow halted before execution.`
|
|
948
|
-
);
|
|
949
1397
|
}
|
|
950
1398
|
}
|
|
1399
|
+
|
|
1400
|
+
return {
|
|
1401
|
+
passed: true,
|
|
1402
|
+
redactions: Object.keys(allRedactions).length > 0 ? allRedactions : null
|
|
1403
|
+
};
|
|
951
1404
|
}
|
|
952
|
-
return { passed: true };
|
|
953
|
-
}
|
|
954
1405
|
|
|
955
|
-
//
|
|
956
|
-
// ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION
|
|
957
|
-
//
|
|
1406
|
+
// ================================
|
|
1407
|
+
// ✅ UPDATED v1.3.0 — KERNEL-LEVEL LLM HALLUCINATION PREVENTION
|
|
1408
|
+
// (CONJUGATION-AWARE + EVASION-RESISTANT)
|
|
1409
|
+
//
|
|
1410
|
+
// Now uses _getFinancialIntentPatterns() instead of inline duplicate.
|
|
1411
|
+
// Xhosa patterns included automatically via the shared set.
|
|
1412
|
+
// All __verified_intent logic unchanged for backward compat.
|
|
1413
|
+
// ================================
|
|
958
1414
|
_validateLLMOutput(output, actionContext) {
|
|
959
1415
|
if (!output || typeof output !== 'string') return { passed: true };
|
|
960
1416
|
|
|
961
1417
|
// ── __verified_intent takes priority ──────────────────────────────────────
|
|
962
|
-
const intent = this.context.__verified_intent;
|
|
963
|
-
if (intent) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1418
|
+
const intent = this.context.__verified_intent;
|
|
1419
|
+
if (intent) {
|
|
1420
|
+
if (intent.prohibited_actions && Array.isArray(intent.prohibited_actions)) {
|
|
1421
|
+
const lower = output.toLowerCase();
|
|
1422
|
+
for (const action of intent.prohibited_actions) {
|
|
1423
|
+
if (lower.includes(action.toLowerCase())) {
|
|
1424
|
+
return {
|
|
1425
|
+
passed: false,
|
|
1426
|
+
reason: `Output violates prohibited action "${action}" defined in __verified_intent`,
|
|
1427
|
+
detected: action,
|
|
1428
|
+
language: 'multi'
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
974
1432
|
}
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
1433
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1434
|
+
if (intent.prohibited_topics && Array.isArray(intent.prohibited_topics)) {
|
|
1435
|
+
for (const topic of intent.prohibited_topics) {
|
|
1436
|
+
const isRegex = typeof topic === 'object' && topic.pattern;
|
|
1437
|
+
let matched = false;
|
|
1438
|
+
let detected = '';
|
|
1439
|
+
|
|
1440
|
+
if (isRegex) {
|
|
1441
|
+
try {
|
|
1442
|
+
const re = new RegExp(topic.pattern, topic.flags || 'i');
|
|
1443
|
+
const match = output.match(re);
|
|
1444
|
+
matched = !!match;
|
|
1445
|
+
detected = match ? match[0] : topic.pattern;
|
|
1446
|
+
} catch (e) {
|
|
1447
|
+
this.addWarning(`Invalid prohibited_topic regex: "${topic.pattern}" — ${e.message}`);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
} else {
|
|
1451
|
+
matched = output.toLowerCase().includes(topic.toLowerCase());
|
|
1452
|
+
detected = topic;
|
|
1453
|
+
}
|
|
983
1454
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
continue;
|
|
1455
|
+
if (matched) {
|
|
1456
|
+
return {
|
|
1457
|
+
passed: false,
|
|
1458
|
+
reason: `Output violates prohibited topic "${isRegex ? topic.pattern : topic}" defined in __verified_intent`,
|
|
1459
|
+
detected,
|
|
1460
|
+
language: 'multi'
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
993
1463
|
}
|
|
994
|
-
} else {
|
|
995
|
-
matched = output.toLowerCase().includes(topic.toLowerCase());
|
|
996
|
-
detected = topic;
|
|
997
1464
|
}
|
|
998
1465
|
|
|
999
|
-
|
|
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
|
-
}
|
|
1466
|
+
// __verified_intent present and passed — skip hardcoded patterns
|
|
1467
|
+
return { passed: true };
|
|
1007
1468
|
}
|
|
1008
|
-
}
|
|
1009
1469
|
|
|
1010
|
-
|
|
1011
|
-
return { passed: true };
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// ── No __verified_intent — fall through to hardcoded patterns ─────────────
|
|
1470
|
+
// ── No __verified_intent — fall through to shared pattern set ─────────────
|
|
1015
1471
|
// 🔑 Extract allowed capabilities from workflow allowlist
|
|
1016
1472
|
const allowedCapabilities = Array.from(this.allowedResolvers)
|
|
1017
1473
|
.filter(name => !name.startsWith('llm-') && name !== 'builtInMathResolver')
|
|
1018
1474
|
.map(name => name.replace('@o-lang/', '').replace(/-resolver$/, ''));
|
|
1019
1475
|
|
|
1020
|
-
// 🔒
|
|
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) {
|
|
1476
|
+
// 🔒 SCAN OUTPUT FOR FORBIDDEN INTENTS (shared set — includes Xhosa)
|
|
1477
|
+
for (const { pattern, capability, lang } of this._getFinancialIntentPatterns()) {
|
|
1112
1478
|
if (pattern.test(output)) {
|
|
1113
1479
|
const hasCapability = allowedCapabilities.some(c =>
|
|
1114
1480
|
c.includes(capability) ||
|
|
@@ -1121,9 +1487,9 @@ const forbiddenPatterns = [
|
|
|
1121
1487
|
|
|
1122
1488
|
if (!hasCapability) {
|
|
1123
1489
|
const match = output.match(pattern);
|
|
1124
|
-
|
|
1490
|
+
|
|
1125
1491
|
// ✅ Explicitly flag African & Financial context for Audit Logs
|
|
1126
|
-
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'am', 'om', 'ff', 'so', 'sn'].includes(lang);
|
|
1492
|
+
const isAfrican = ['yo', 'ig', 'ha', 'sw', 'zu', 'xh', 'am', 'om', 'ff', 'so', 'sn','tw'].includes(lang);
|
|
1127
1493
|
const isFinancial = ['transfer', 'payment', 'withdrawal', 'deposit', 'financial_action'].includes(capability);
|
|
1128
1494
|
|
|
1129
1495
|
return {
|
|
@@ -1131,9 +1497,9 @@ const forbiddenPatterns = [
|
|
|
1131
1497
|
reason: `Hallucinated "${capability}" capability in ${lang}...`,
|
|
1132
1498
|
detected: match ? match[0].trim() : 'unknown pattern',
|
|
1133
1499
|
language: lang,
|
|
1134
|
-
african_language_detected: isAfrican,
|
|
1500
|
+
african_language_detected: isAfrican,
|
|
1135
1501
|
financial_expression_found: isFinancial,
|
|
1136
|
-
capability_attempted: capability
|
|
1502
|
+
capability_attempted: capability
|
|
1137
1503
|
};
|
|
1138
1504
|
}
|
|
1139
1505
|
}
|
|
@@ -1144,22 +1510,22 @@ const forbiddenPatterns = [
|
|
|
1144
1510
|
// -----------------------------
|
|
1145
1511
|
// ✅ CRITICAL FIX: Resolver output unwrapping helper
|
|
1146
1512
|
// -----------------------------
|
|
1147
|
-
_unwrapResolverResult(result) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1513
|
+
_unwrapResolverResult(result) {
|
|
1514
|
+
if (result && typeof result === 'object') {
|
|
1515
|
+
if (result.output !== undefined) return result.output;
|
|
1516
|
+
if (result.response !== undefined) return result.response; // ✅ ADD THIS
|
|
1517
|
+
if (result.text !== undefined) return result.text;
|
|
1518
|
+
if (result.content !== undefined) return result.content;
|
|
1519
|
+
}
|
|
1520
|
+
return result;
|
|
1153
1521
|
}
|
|
1154
|
-
return result;
|
|
1155
|
-
}
|
|
1156
1522
|
|
|
1157
1523
|
// -----------------------------
|
|
1158
1524
|
// Step execution (WHERE RESOLVERS ARE INVOKED)
|
|
1159
1525
|
// -----------------------------
|
|
1160
1526
|
async executeStep(step, agentResolver) {
|
|
1161
1527
|
const stepType = step.type;
|
|
1162
|
-
|
|
1528
|
+
|
|
1163
1529
|
// ✅ Enforce per-step constraints (basic validation)
|
|
1164
1530
|
if (step.constraints && Object.keys(step.constraints).length > 0) {
|
|
1165
1531
|
for (const [key, value] of Object.entries(step.constraints)) {
|
|
@@ -1235,7 +1601,7 @@ _unwrapResolverResult(result) {
|
|
|
1235
1601
|
this.context[`__resolver_${idx}`] = result;
|
|
1236
1602
|
return this._unwrapResolverResult(result);
|
|
1237
1603
|
}
|
|
1238
|
-
|
|
1604
|
+
|
|
1239
1605
|
resolverAttempts.push({
|
|
1240
1606
|
name: resolverName,
|
|
1241
1607
|
status: 'skipped',
|
|
@@ -1303,7 +1669,7 @@ _unwrapResolverResult(result) {
|
|
|
1303
1669
|
errorMessage += ` → Verify API keys/tokens are set in environment variables\n`;
|
|
1304
1670
|
}
|
|
1305
1671
|
}
|
|
1306
|
-
|
|
1672
|
+
|
|
1307
1673
|
errorMessage += `\n• Resolver documentation:\n`;
|
|
1308
1674
|
let hasDocs = false;
|
|
1309
1675
|
resolverAttempts.forEach(attempt => {
|
|
@@ -1319,7 +1685,7 @@ _unwrapResolverResult(result) {
|
|
|
1319
1685
|
throw new Error(errorMessage);
|
|
1320
1686
|
};
|
|
1321
1687
|
|
|
1322
|
-
|
|
1688
|
+
switch (stepType) {
|
|
1323
1689
|
case 'calculate': {
|
|
1324
1690
|
// ✅ Interpolate variables in the expression before evaluation
|
|
1325
1691
|
let expr = step.expression || step.actionRaw;
|
|
@@ -1330,7 +1696,7 @@ _unwrapResolverResult(result) {
|
|
|
1330
1696
|
if (step.saveAs) this.context[step.saveAs] = result;
|
|
1331
1697
|
break;
|
|
1332
1698
|
}
|
|
1333
|
-
|
|
1699
|
+
|
|
1334
1700
|
case 'action': {
|
|
1335
1701
|
let action = this._safeInterpolate(
|
|
1336
1702
|
step.actionRaw,
|
|
@@ -1428,7 +1794,7 @@ _unwrapResolverResult(result) {
|
|
|
1428
1794
|
}
|
|
1429
1795
|
break;
|
|
1430
1796
|
}
|
|
1431
|
-
|
|
1797
|
+
|
|
1432
1798
|
case 'use': {
|
|
1433
1799
|
const tool = this._safeInterpolate(step.tool, this.context, 'tool name');
|
|
1434
1800
|
const rawResult = await runResolvers(`Use ${tool}`);
|
|
@@ -1436,7 +1802,7 @@ _unwrapResolverResult(result) {
|
|
|
1436
1802
|
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
1437
1803
|
break;
|
|
1438
1804
|
}
|
|
1439
|
-
|
|
1805
|
+
|
|
1440
1806
|
case 'ask': {
|
|
1441
1807
|
const target = this._safeInterpolate(step.target, this.context, 'LLM prompt');
|
|
1442
1808
|
if (/{[^}]+}/.test(target)) {
|
|
@@ -1461,7 +1827,7 @@ _unwrapResolverResult(result) {
|
|
|
1461
1827
|
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
1462
1828
|
break;
|
|
1463
1829
|
}
|
|
1464
|
-
|
|
1830
|
+
|
|
1465
1831
|
case 'evolve': {
|
|
1466
1832
|
const { targetResolver, feedback } = step;
|
|
1467
1833
|
if (this.verbose) {
|
|
@@ -1483,173 +1849,655 @@ _unwrapResolverResult(result) {
|
|
|
1483
1849
|
}
|
|
1484
1850
|
break;
|
|
1485
1851
|
}
|
|
1486
|
-
|
|
1852
|
+
|
|
1853
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1854
|
+
// IF / ELSE-IF / ELSE
|
|
1855
|
+
//
|
|
1856
|
+
// Governance Features:
|
|
1857
|
+
// 1. Exclusive Branching: Only one branch executes (if -> else-if -> else).
|
|
1858
|
+
// 2. Semantic Validation: Checks symbols in conditions before evaluation.
|
|
1859
|
+
// 3. Auditability: Logs which condition was evaluated and which branch fired.
|
|
1860
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1861
|
+
|
|
1487
1862
|
case 'if': {
|
|
1488
|
-
|
|
1489
|
-
|
|
1863
|
+
// 1. Validate all symbols referenced in the main condition
|
|
1864
|
+
const condSymbols = step.condition ? step.condition.match(/\{([^\}]+)\}/g) || [] : [];
|
|
1865
|
+
let symbolsValid = true;
|
|
1866
|
+
|
|
1867
|
+
for (const sym of condSymbols) {
|
|
1868
|
+
const key = sym.replace(/[{}]/g, '');
|
|
1869
|
+
if (!this._requireSemantic(key, 'if condition')) {
|
|
1870
|
+
symbolsValid = false;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (!symbolsValid) {
|
|
1875
|
+
this._createAuditEntry('condition_skipped', {
|
|
1876
|
+
condition: step.condition,
|
|
1877
|
+
reason: 'One or more symbols missing in context',
|
|
1878
|
+
severity: 'warn'
|
|
1879
|
+
});
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// 2. Evaluate main if condition
|
|
1884
|
+
const mainPassed = this.evaluateCondition(step.condition, this.context);
|
|
1885
|
+
|
|
1886
|
+
this._createAuditEntry('condition_evaluated', {
|
|
1887
|
+
condition: step.condition,
|
|
1888
|
+
passed: mainPassed,
|
|
1889
|
+
branch: 'if',
|
|
1890
|
+
severity: 'info'
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
if (mainPassed) {
|
|
1894
|
+
if (step.body && Array.isArray(step.body)) {
|
|
1895
|
+
for (const s of step.body) await this.executeStep(s, agentResolver);
|
|
1896
|
+
}
|
|
1897
|
+
break; // Exit after successful if
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// 3. else-if chain — stop at first match
|
|
1901
|
+
if (step.elseIf && Array.isArray(step.elseIf)) {
|
|
1902
|
+
let elseIfFired = false;
|
|
1903
|
+
for (const branch of step.elseIf) {
|
|
1904
|
+
// Validate symbols for else-if branch
|
|
1905
|
+
const branchSymbols = branch.condition ? branch.condition.match(/\{([^\}]+)\}/g) || [] : [];
|
|
1906
|
+
let branchSymbolsValid = true;
|
|
1907
|
+
for (const sym of branchSymbols) {
|
|
1908
|
+
const key = sym.replace(/[{}]/g, '');
|
|
1909
|
+
if (!this._requireSemantic(key, 'else-if condition')) {
|
|
1910
|
+
branchSymbolsValid = false;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (!branchSymbolsValid) {
|
|
1915
|
+
this._createAuditEntry('condition_skipped', {
|
|
1916
|
+
condition: branch.condition,
|
|
1917
|
+
reason: 'One or more symbols missing in context',
|
|
1918
|
+
severity: 'warn'
|
|
1919
|
+
});
|
|
1920
|
+
continue; // Skip this else-if, try next
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const branchPassed = this.evaluateCondition(branch.condition, this.context);
|
|
1924
|
+
|
|
1925
|
+
this._createAuditEntry('condition_evaluated', {
|
|
1926
|
+
condition: branch.condition,
|
|
1927
|
+
passed: branchPassed,
|
|
1928
|
+
branch: 'else-if',
|
|
1929
|
+
severity: 'info'
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
if (branchPassed) {
|
|
1933
|
+
if (branch.body && Array.isArray(branch.body)) {
|
|
1934
|
+
for (const s of branch.body) await this.executeStep(s, agentResolver);
|
|
1935
|
+
}
|
|
1936
|
+
elseIfFired = true;
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (elseIfFired) break;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// 4. else fallback
|
|
1944
|
+
if (step.elseBranch && Array.isArray(step.elseBranch)) {
|
|
1945
|
+
this._createAuditEntry('condition_evaluated', {
|
|
1946
|
+
condition: 'else',
|
|
1947
|
+
passed: true,
|
|
1948
|
+
branch: 'else',
|
|
1949
|
+
severity: 'info'
|
|
1950
|
+
});
|
|
1951
|
+
for (const s of step.elseBranch) await this.executeStep(s, agentResolver);
|
|
1490
1952
|
}
|
|
1953
|
+
|
|
1491
1954
|
break;
|
|
1492
1955
|
}
|
|
1493
|
-
|
|
1956
|
+
|
|
1957
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1958
|
+
// PARALLEL
|
|
1959
|
+
//
|
|
1960
|
+
// Bugs fixed:
|
|
1961
|
+
// 1. Shared this.context race condition — all branches wrote to the same
|
|
1962
|
+
// object concurrently. Branch 2 restoring the snapshot overwrote whatever
|
|
1963
|
+
// branch 1 had just saved, so result_a was lost by the time it was read.
|
|
1964
|
+
// Fix: each branch gets its own context via Object.create(this), which
|
|
1965
|
+
// prototype-links to the parent (sharing allowedResolvers, auditLog,
|
|
1966
|
+
// events, verbose) but has its own context property that shadows the
|
|
1967
|
+
// parent's. Branches never touch this.context directly.
|
|
1968
|
+
//
|
|
1969
|
+
// 2. Promise.all → silent failure swallowing. A single rejection cancelled
|
|
1970
|
+
// all siblings. Fix: buildStepPromise catches internally and returns a
|
|
1971
|
+
// structured outcome, so Promise.all always resolves with the full set.
|
|
1972
|
+
//
|
|
1973
|
+
// 3. timed_out not always written. Fix: always written after settlement —
|
|
1974
|
+
// true on timeout, false on clean completion.
|
|
1975
|
+
//
|
|
1976
|
+
// 4. Losing Promise.race branch kept mutating this.context after the
|
|
1977
|
+
// workflow moved on. Fix: branches write to branchRuntime.context only;
|
|
1978
|
+
// this.context is only touched during the final merge step.
|
|
1979
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1980
|
+
|
|
1494
1981
|
case 'parallel': {
|
|
1495
1982
|
const { steps, timeout } = step;
|
|
1983
|
+
|
|
1984
|
+
if (!steps || !Array.isArray(steps) || steps.length === 0) {
|
|
1985
|
+
this.addWarning('Parallel step contains no sub-steps. Skipping.');
|
|
1986
|
+
break;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// Snapshot context before any branch runs.
|
|
1990
|
+
// Every branch reads from this — not from each other.
|
|
1991
|
+
const contextSnapshot = { ...this.context };
|
|
1992
|
+
|
|
1993
|
+
// Build one promise per sub-step.
|
|
1994
|
+
//
|
|
1995
|
+
// Each branch runs against a prototype-linked clone of the runtime.
|
|
1996
|
+
// Object.create(this) shares: allowedResolvers, auditLog, events,
|
|
1997
|
+
// verbose, resources, agentMap — everything a step needs to execute.
|
|
1998
|
+
// But branchRuntime.context is its own property that shadows this.context,
|
|
1999
|
+
// so concurrent writes never collide.
|
|
2000
|
+
//
|
|
2001
|
+
// Errors are caught here and returned as structured outcomes so that
|
|
2002
|
+
// Promise.all always resolves with the full result set — one failure
|
|
2003
|
+
// does not cancel sibling branches.
|
|
2004
|
+
const buildStepPromise = async (s, index) => {
|
|
2005
|
+
const branchRuntime = Object.create(this);
|
|
2006
|
+
branchRuntime.context = { ...contextSnapshot };
|
|
2007
|
+
|
|
2008
|
+
try {
|
|
2009
|
+
await branchRuntime.executeStep(s, agentResolver);
|
|
2010
|
+
|
|
2011
|
+
const saveKey = s.saveAs || null;
|
|
2012
|
+
const value = saveKey ? branchRuntime.context[saveKey] : undefined;
|
|
2013
|
+
|
|
2014
|
+
return {
|
|
2015
|
+
status: 'fulfilled',
|
|
2016
|
+
index,
|
|
2017
|
+
saveAs: saveKey,
|
|
2018
|
+
value,
|
|
2019
|
+
stepType: s.type
|
|
2020
|
+
};
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
return {
|
|
2023
|
+
status: 'rejected',
|
|
2024
|
+
index,
|
|
2025
|
+
reason: error.message || String(error),
|
|
2026
|
+
stepType: s.type
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
const runAllSteps = () =>
|
|
2032
|
+
Promise.all(steps.map((s, i) => buildStepPromise(s, i)));
|
|
2033
|
+
|
|
2034
|
+
let settledResults;
|
|
2035
|
+
|
|
1496
2036
|
if (timeout !== undefined && timeout > 0) {
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
)
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
2037
|
+
// Race: all steps vs timeout sentinel.
|
|
2038
|
+
// runAllSteps never rejects (errors caught inside buildStepPromise),
|
|
2039
|
+
// so Promise.race resolves with either the results array or null.
|
|
2040
|
+
const timeoutPromise = new Promise(resolve =>
|
|
2041
|
+
setTimeout(() => resolve(null), timeout)
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
settledResults = await Promise.race([runAllSteps(), timeoutPromise]);
|
|
2045
|
+
|
|
2046
|
+
if (settledResults === null) {
|
|
2047
|
+
// Timeout won. Restore snapshot + mark timed_out.
|
|
2048
|
+
// Do NOT merge partial results — we cannot know which branches
|
|
2049
|
+
// completed cleanly before the cutoff.
|
|
2050
|
+
this.context = { ...contextSnapshot, timed_out: true };
|
|
2051
|
+
|
|
2052
|
+
this.emit('parallel_timeout', {
|
|
2053
|
+
duration: timeout,
|
|
2054
|
+
steps_count: steps.length
|
|
2055
|
+
});
|
|
2056
|
+
|
|
1507
2057
|
if (this.verbose) {
|
|
1508
2058
|
console.log(`⏰ Parallel execution timed out after ${timeout}ms`);
|
|
1509
2059
|
}
|
|
2060
|
+
|
|
2061
|
+
this._createAuditEntry('parallel_timeout', {
|
|
2062
|
+
timeout_ms: timeout,
|
|
2063
|
+
steps_count: steps.length,
|
|
2064
|
+
severity: 'warn'
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
break;
|
|
1510
2068
|
}
|
|
2069
|
+
|
|
1511
2070
|
} else {
|
|
1512
|
-
|
|
1513
|
-
|
|
2071
|
+
settledResults = await runAllSteps();
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Merge results back into this.context.
|
|
2075
|
+
// Start from the snapshot so pre-parallel state is the clean base,
|
|
2076
|
+
// then layer each branch's saveAs result on top.
|
|
2077
|
+
this.context = { ...contextSnapshot, timed_out: false };
|
|
2078
|
+
|
|
2079
|
+
for (const outcome of settledResults) {
|
|
2080
|
+
if (outcome.status === 'fulfilled') {
|
|
2081
|
+
const { saveAs, value } = outcome;
|
|
2082
|
+
if (saveAs !== null && value !== undefined) {
|
|
2083
|
+
this.context[saveAs] = value;
|
|
2084
|
+
}
|
|
2085
|
+
} else {
|
|
2086
|
+
const { reason, index } = outcome;
|
|
2087
|
+
this.addWarning(`Parallel step [index ${index}] failed: ${reason}`);
|
|
2088
|
+
|
|
2089
|
+
this.emit('parallel_step_failed', {
|
|
2090
|
+
index,
|
|
2091
|
+
reason,
|
|
2092
|
+
stepType: outcome.stepType
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
this._createAuditEntry('parallel_step_failed', {
|
|
2096
|
+
step_index: index,
|
|
2097
|
+
reason,
|
|
2098
|
+
severity: 'high'
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
1514
2101
|
}
|
|
2102
|
+
|
|
1515
2103
|
break;
|
|
1516
2104
|
}
|
|
1517
|
-
|
|
2105
|
+
|
|
2106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2107
|
+
// ESCALATION
|
|
2108
|
+
//
|
|
2109
|
+
// Governance Features:
|
|
2110
|
+
// 1. Scope Safety: Fixes ReferenceError by declaring levelSteps in outer scope.
|
|
2111
|
+
// 2. Auditability: Logs level start, completion, timeout, and final outcome.
|
|
2112
|
+
// 3. Determinism: Ensures timed-out levels don't corrupt context.
|
|
2113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2114
|
+
|
|
1518
2115
|
case 'escalation': {
|
|
1519
2116
|
const { levels } = step;
|
|
2117
|
+
const { parseBlock } = require('../parser');
|
|
2118
|
+
|
|
1520
2119
|
let finalResult = null;
|
|
1521
|
-
let currentTimeout = 0;
|
|
1522
2120
|
let completedLevel = null;
|
|
2121
|
+
|
|
1523
2122
|
for (const level of levels) {
|
|
2123
|
+
// Fix: Declare levelSteps in outer block scope to avoid ReferenceError
|
|
2124
|
+
// in the timed-out branch.
|
|
2125
|
+
const levelSteps = parseBlock(level.steps);
|
|
2126
|
+
|
|
2127
|
+
this._createAuditEntry('escalation_level_started', {
|
|
2128
|
+
level: level.levelNumber,
|
|
2129
|
+
timeout_ms: level.timeout,
|
|
2130
|
+
steps_count: levelSteps.length,
|
|
2131
|
+
severity: 'info'
|
|
2132
|
+
});
|
|
2133
|
+
|
|
1524
2134
|
if (level.timeout === 0) {
|
|
1525
|
-
|
|
2135
|
+
// Immediate level — execute sequentially
|
|
1526
2136
|
for (const levelStep of levelSteps) {
|
|
1527
2137
|
await this.executeStep(levelStep, agentResolver);
|
|
1528
2138
|
}
|
|
2139
|
+
|
|
2140
|
+
// Check if result was saved
|
|
1529
2141
|
if (levelSteps.length > 0) {
|
|
1530
2142
|
const lastStep = levelSteps[levelSteps.length - 1];
|
|
1531
2143
|
if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
|
|
1532
2144
|
finalResult = this.context[lastStep.saveAs];
|
|
1533
2145
|
completedLevel = level.levelNumber;
|
|
1534
|
-
|
|
2146
|
+
|
|
2147
|
+
this._createAuditEntry('escalation_level_completed', {
|
|
2148
|
+
level: level.levelNumber,
|
|
2149
|
+
timed_out: false,
|
|
2150
|
+
severity: 'info'
|
|
2151
|
+
});
|
|
2152
|
+
break; // Escalation complete
|
|
1535
2153
|
}
|
|
1536
2154
|
}
|
|
2155
|
+
|
|
1537
2156
|
} else {
|
|
1538
|
-
|
|
1539
|
-
const timeoutPromise = new Promise(resolve =>
|
|
1540
|
-
setTimeout(() => resolve({ timedOut: true }), level.timeout)
|
|
1541
|
-
|
|
2157
|
+
// Timed level
|
|
2158
|
+
const timeoutPromise = new Promise(resolve =>
|
|
2159
|
+
setTimeout(() => resolve({ timedOut: true }), level.timeout)
|
|
2160
|
+
);
|
|
2161
|
+
|
|
1542
2162
|
const levelPromise = (async () => {
|
|
1543
|
-
const levelSteps = require('./parser').parseBlock(level.steps);
|
|
1544
2163
|
for (const levelStep of levelSteps) {
|
|
1545
2164
|
await this.executeStep(levelStep, agentResolver);
|
|
1546
2165
|
}
|
|
1547
2166
|
return { timedOut: false };
|
|
1548
2167
|
})();
|
|
2168
|
+
|
|
1549
2169
|
const result = await Promise.race([timeoutPromise, levelPromise]);
|
|
2170
|
+
|
|
1550
2171
|
if (!result.timedOut) {
|
|
1551
|
-
|
|
2172
|
+
// Level completed within time
|
|
2173
|
+
if (levelSteps.length > 0) {
|
|
1552
2174
|
const lastStep = levelSteps[levelSteps.length - 1];
|
|
1553
2175
|
if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
|
|
1554
2176
|
finalResult = this.context[lastStep.saveAs];
|
|
1555
2177
|
completedLevel = level.levelNumber;
|
|
1556
|
-
|
|
2178
|
+
|
|
2179
|
+
this._createAuditEntry('escalation_level_completed', {
|
|
2180
|
+
level: level.levelNumber,
|
|
2181
|
+
timed_out: false,
|
|
2182
|
+
severity: 'info'
|
|
2183
|
+
});
|
|
2184
|
+
break; // Escalation complete
|
|
1557
2185
|
}
|
|
1558
2186
|
}
|
|
2187
|
+
} else {
|
|
2188
|
+
// Level timed out
|
|
2189
|
+
this._createAuditEntry('escalation_level_timeout', {
|
|
2190
|
+
level: level.levelNumber,
|
|
2191
|
+
timeout_ms: level.timeout,
|
|
2192
|
+
severity: 'warn'
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
if (this.verbose) {
|
|
2196
|
+
console.log(
|
|
2197
|
+
`⏰ Escalation level ${level.levelNumber} timed out ` +
|
|
2198
|
+
`after ${level.timeout}ms — trying next level`
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
// Continue to next level
|
|
1559
2202
|
}
|
|
1560
2203
|
}
|
|
1561
2204
|
}
|
|
2205
|
+
|
|
2206
|
+
// Final context state
|
|
1562
2207
|
this.context.escalation_completed = finalResult !== null;
|
|
1563
2208
|
this.context.timed_out = finalResult === null;
|
|
1564
2209
|
if (completedLevel !== null) {
|
|
1565
2210
|
this.context.escalation_level = completedLevel;
|
|
1566
2211
|
}
|
|
2212
|
+
|
|
2213
|
+
this._createAuditEntry('escalation_outcome', {
|
|
2214
|
+
completed: finalResult !== null,
|
|
2215
|
+
completed_at_level: completedLevel,
|
|
2216
|
+
timed_out: finalResult === null,
|
|
2217
|
+
severity: finalResult !== null ? 'info' : 'warn'
|
|
2218
|
+
});
|
|
2219
|
+
|
|
1567
2220
|
break;
|
|
1568
2221
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
2222
|
+
|
|
2223
|
+
case 'connect': {
|
|
2224
|
+
if (!step.resource || !step.endpoint) {
|
|
2225
|
+
this.addWarning('Connect step missing "resource" or "endpoint". Skipping.');
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// Only validate URL format for url-type connects.
|
|
2230
|
+
// Resolver-type endpoints are package names (@o-lang/kyc-resolver)
|
|
2231
|
+
// and are not valid URLs — do not run them through new URL().
|
|
2232
|
+
if (!step.targetType || step.targetType === 'url') {
|
|
2233
|
+
try {
|
|
2234
|
+
new URL(step.endpoint);
|
|
2235
|
+
} catch (e) {
|
|
2236
|
+
this.addWarning(`Connect: Invalid endpoint URL for "${step.resource}": ${step.endpoint}`);
|
|
2237
|
+
break;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
this.resources[step.resource] = step.endpoint;
|
|
2242
|
+
|
|
2243
|
+
this._createAuditEntry('resource_connected', {
|
|
2244
|
+
resource: step.resource,
|
|
2245
|
+
target_type: step.targetType || 'url',
|
|
2246
|
+
endpoint_masked: step.endpoint.replace(/\/\/[^@]+@/, '//***@'),
|
|
2247
|
+
severity: 'info'
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
if (this.verbose) {
|
|
2251
|
+
console.log(`🔗 Connected "${step.resource}" → ${step.endpoint}`);
|
|
2252
|
+
}
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2256
|
+
// AGENT_USE
|
|
2257
|
+
// Maps a logical agent name (e.g., "support_bot") to a registered resource.
|
|
2258
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1575
2259
|
case 'agent_use': {
|
|
2260
|
+
if (!step.logicalName || !step.resource) {
|
|
2261
|
+
this.addWarning('Agent_use step missing "logicalName" or "resource". Skipping.');
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// Optional: Validate that the resource was previously connected
|
|
2266
|
+
if (!this.resources[step.resource]) {
|
|
2267
|
+
this.addWarning(`Agent_use: Resource "${step.resource}" has not been connected yet.`);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
1576
2270
|
this.agentMap[step.logicalName] = step.resource;
|
|
2271
|
+
|
|
2272
|
+
// ✅ AUDIT LOG: Agent Mapping
|
|
2273
|
+
this._createAuditEntry('agent_mapped', {
|
|
2274
|
+
logical_name: step.logicalName,
|
|
2275
|
+
resource: step.resource,
|
|
2276
|
+
severity: 'info'
|
|
2277
|
+
});
|
|
2278
|
+
|
|
2279
|
+
if (this.verbose) {
|
|
2280
|
+
console.log(`🤖 Mapped agent "${step.logicalName}" to resource "${step.resource}"`);
|
|
2281
|
+
}
|
|
1577
2282
|
break;
|
|
1578
2283
|
}
|
|
1579
|
-
|
|
2284
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2285
|
+
// DEBRIEF
|
|
2286
|
+
//
|
|
2287
|
+
// Governance Features:
|
|
2288
|
+
// 1. Semantic Validation: Ensures all {symbols} exist before emitting.
|
|
2289
|
+
// 2. Interpolation: Agents receive resolved values, not raw templates.
|
|
2290
|
+
// 3. Auditability: Every debrief is logged for compliance tracing.
|
|
2291
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2292
|
+
|
|
1580
2293
|
case 'debrief': {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2294
|
+
const messageTemplate = step.message;
|
|
2295
|
+
|
|
2296
|
+
// 1. Validate all referenced symbols exist in context
|
|
2297
|
+
if (messageTemplate && messageTemplate.includes('{')) {
|
|
2298
|
+
const symbolMatches = messageTemplate.match(/\{([^\}]+)\}/g) || [];
|
|
2299
|
+
|
|
2300
|
+
// Check every symbol. _requireSemantic will emit 'semantic_violation'
|
|
2301
|
+
// if a symbol is missing, but we also need to stop execution here.
|
|
2302
|
+
const allPresent = symbolMatches.every(sym => {
|
|
2303
|
+
const key = sym.replace(/[{}]/g, '');
|
|
2304
|
+
return this._requireSemantic(key, 'debrief');
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
if (!allPresent) {
|
|
2308
|
+
if (this.verbose) {
|
|
2309
|
+
console.log(`⏭️ Debrief skipped — one or more symbols missing in context`);
|
|
2310
|
+
}
|
|
2311
|
+
// Break early. Do not emit incomplete data.
|
|
2312
|
+
break;
|
|
1586
2313
|
}
|
|
1587
2314
|
}
|
|
1588
|
-
|
|
2315
|
+
|
|
2316
|
+
// 2. Interpolate — agent receives the real value, not the template
|
|
2317
|
+
const interpolatedMessage = this._safeInterpolate(
|
|
2318
|
+
messageTemplate,
|
|
2319
|
+
this.context,
|
|
2320
|
+
'debrief message'
|
|
2321
|
+
);
|
|
2322
|
+
|
|
2323
|
+
// 3. Audit trail — every agent message must be traceable
|
|
2324
|
+
this._createAuditEntry('debrief_emitted', {
|
|
2325
|
+
agent: step.agent,
|
|
2326
|
+
message_length: interpolatedMessage ? interpolatedMessage.length : 0,
|
|
2327
|
+
symbols_resolved: (messageTemplate.match(/\{([^\}]+)\}/g) || []).length,
|
|
2328
|
+
severity: 'info'
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
this.emit('debrief', {
|
|
2332
|
+
agent: step.agent,
|
|
2333
|
+
message: interpolatedMessage
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
if (this.verbose) {
|
|
2337
|
+
console.log(`📨 Debrief → agent "${step.agent}": ${interpolatedMessage}`);
|
|
2338
|
+
}
|
|
1589
2339
|
break;
|
|
1590
2340
|
}
|
|
1591
|
-
|
|
2341
|
+
|
|
2342
|
+
|
|
1592
2343
|
case 'prompt': {
|
|
1593
2344
|
if (this.verbose) {
|
|
1594
2345
|
console.log(`❓ Prompt: ${step.question}`);
|
|
1595
2346
|
}
|
|
1596
2347
|
break;
|
|
1597
2348
|
}
|
|
1598
|
-
|
|
2349
|
+
|
|
2350
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2351
|
+
// EMIT
|
|
2352
|
+
//
|
|
2353
|
+
// Governance Features:
|
|
2354
|
+
// 1. Semantic Validation: Stops at first missing symbol to reduce noise.
|
|
2355
|
+
// 2. Auditability: External events are logged with payload metadata.
|
|
2356
|
+
// 3. Consistency: Uses same interpolation logic as debrief.
|
|
2357
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2358
|
+
|
|
1599
2359
|
case 'emit': {
|
|
1600
2360
|
const payloadTemplate = step.payload;
|
|
2361
|
+
|
|
2362
|
+
// Extract unique symbols from the payload template
|
|
1601
2363
|
const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (!
|
|
2364
|
+
|
|
2365
|
+
// Validate all symbols, stop at first missing one
|
|
2366
|
+
const allPresent = symbols.every(sym => {
|
|
2367
|
+
const key = sym.replace(/[{}]/g, '');
|
|
2368
|
+
return this._requireSemantic(key, 'emit');
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
if (!allPresent) {
|
|
1610
2372
|
if (this.verbose) {
|
|
1611
|
-
console.log(`⏭️
|
|
2373
|
+
console.log(`⏭️ Skipped emit due to missing semantic symbols`);
|
|
1612
2374
|
}
|
|
1613
2375
|
break;
|
|
1614
2376
|
}
|
|
1615
|
-
|
|
2377
|
+
|
|
2378
|
+
// Interpolate the payload
|
|
2379
|
+
const payload = this._safeInterpolate(
|
|
2380
|
+
payloadTemplate,
|
|
2381
|
+
this.context,
|
|
2382
|
+
'emit payload'
|
|
2383
|
+
);
|
|
2384
|
+
|
|
2385
|
+
// ✅ AUDIT LOG: External event emission
|
|
2386
|
+
this._createAuditEntry('event_emitted', {
|
|
2387
|
+
event: step.event,
|
|
2388
|
+
payload_length: payload ? payload.length : 0,
|
|
2389
|
+
symbols_resolved: symbols.length,
|
|
2390
|
+
severity: 'info'
|
|
2391
|
+
});
|
|
2392
|
+
|
|
1616
2393
|
this.emit(step.event, {
|
|
1617
|
-
payload
|
|
1618
|
-
workflow: this.context.workflow_name,
|
|
2394
|
+
payload,
|
|
2395
|
+
workflow: this.context.workflow_name || 'unknown',
|
|
1619
2396
|
timestamp: new Date().toISOString()
|
|
1620
2397
|
});
|
|
2398
|
+
|
|
1621
2399
|
if (this.verbose) {
|
|
1622
2400
|
console.log(`📤 Emit event "${step.event}" with payload: ${payload}`);
|
|
1623
2401
|
}
|
|
1624
2402
|
break;
|
|
1625
2403
|
}
|
|
1626
|
-
|
|
2404
|
+
|
|
2405
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2406
|
+
// PERSIST
|
|
2407
|
+
//
|
|
2408
|
+
// Governance Features:
|
|
2409
|
+
// 1. Data Integrity: Prevents silent corruption of objects into "[object Object]".
|
|
2410
|
+
// 2. Auditability: Logs path, format, and variable name (without raw values).
|
|
2411
|
+
// 3. Safety: Validates semantic existence before attempting I/O.
|
|
2412
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2413
|
+
|
|
1627
2414
|
case 'persist': {
|
|
1628
|
-
|
|
2415
|
+
const fs = require('fs');
|
|
2416
|
+
const path = require('path');
|
|
2417
|
+
|
|
2418
|
+
// 1. Semantic Guard: Ensure the variable exists in context
|
|
2419
|
+
if (!step.variable || !this._requireSemantic(step.variable, 'persist')) {
|
|
1629
2420
|
if (this.verbose) {
|
|
1630
|
-
console.log(`⏭️
|
|
2421
|
+
console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
|
|
1631
2422
|
}
|
|
1632
2423
|
break;
|
|
1633
2424
|
}
|
|
2425
|
+
|
|
1634
2426
|
const sourceValue = this.context[step.variable];
|
|
2427
|
+
|
|
2428
|
+
// Resolve absolute path to prevent directory traversal attacks or ambiguity
|
|
1635
2429
|
const outputPath = path.resolve(process.cwd(), step.target);
|
|
1636
2430
|
const outputDir = path.dirname(outputPath);
|
|
2431
|
+
|
|
2432
|
+
// Ensure directory exists
|
|
1637
2433
|
if (!fs.existsSync(outputDir)) {
|
|
1638
|
-
|
|
2434
|
+
try {
|
|
2435
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
2436
|
+
} catch (e) {
|
|
2437
|
+
this.addWarning(`persist: failed to create directory "${outputDir}": ${e.message}`);
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
1639
2440
|
}
|
|
2441
|
+
|
|
2442
|
+
// 2. Serialize with safe fallback for object → non-JSON targets
|
|
1640
2443
|
let content;
|
|
2444
|
+
const isObject = sourceValue !== null && typeof sourceValue === 'object';
|
|
2445
|
+
let formatUsed = 'string';
|
|
2446
|
+
|
|
1641
2447
|
if (step.target.endsWith('.json')) {
|
|
2448
|
+
// Standard JSON serialization
|
|
1642
2449
|
content = JSON.stringify(sourceValue, null, 2);
|
|
2450
|
+
formatUsed = 'json';
|
|
2451
|
+
} else if (isObject) {
|
|
2452
|
+
// CRITICAL FIX: Prevents "[object Object]" data corruption.
|
|
2453
|
+
// If user tries to save an object to .txt/.csv, we coerce to JSON
|
|
2454
|
+
// so the data remains recoverable, but we warn them.
|
|
2455
|
+
this.addWarning(
|
|
2456
|
+
`persist: "${step.variable}" is an object but target "${step.target}" is not .json. ` +
|
|
2457
|
+
`Writing as JSON to prevent data loss. Rename target to .json or select a specific field.`
|
|
2458
|
+
);
|
|
2459
|
+
content = JSON.stringify(sourceValue, null, 2);
|
|
2460
|
+
formatUsed = 'json-fallback';
|
|
1643
2461
|
} else {
|
|
2462
|
+
// Primitive values (string, number, boolean)
|
|
1644
2463
|
content = String(sourceValue);
|
|
2464
|
+
formatUsed = 'string';
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// 3. Write with error surfacing
|
|
2468
|
+
try {
|
|
2469
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
2470
|
+
} catch (e) {
|
|
2471
|
+
this.addWarning(`persist: failed to write "${step.variable}" to "${step.target}": ${e.message}`);
|
|
2472
|
+
|
|
2473
|
+
// ✅ AUDIT LOG: Failed Write
|
|
2474
|
+
this._createAuditEntry('persist_failed', {
|
|
2475
|
+
variable: step.variable,
|
|
2476
|
+
target: step.target,
|
|
2477
|
+
error: e.message,
|
|
2478
|
+
severity: 'high'
|
|
2479
|
+
});
|
|
2480
|
+
break;
|
|
1645
2481
|
}
|
|
1646
|
-
|
|
2482
|
+
|
|
2483
|
+
// 4. ✅ AUDIT LOG: Successful Write
|
|
2484
|
+
// Note: We do NOT log the actual value content to protect PII.
|
|
2485
|
+
// We log metadata only.
|
|
2486
|
+
this._createAuditEntry('context_persisted', {
|
|
2487
|
+
variable: step.variable,
|
|
2488
|
+
target: step.target,
|
|
2489
|
+
format: formatUsed,
|
|
2490
|
+
value_type: isObject ? 'object' : typeof sourceValue,
|
|
2491
|
+
byte_length: Buffer.byteLength(content, 'utf8'),
|
|
2492
|
+
severity: 'info'
|
|
2493
|
+
});
|
|
2494
|
+
|
|
1647
2495
|
if (this.verbose) {
|
|
1648
|
-
console.log(`💾 Persisted "${step.variable}" to ${step.target}`);
|
|
2496
|
+
console.log(`💾 Persisted "${step.variable}" to ${step.target} (${formatUsed})`);
|
|
1649
2497
|
}
|
|
1650
2498
|
break;
|
|
1651
2499
|
}
|
|
1652
|
-
|
|
2500
|
+
|
|
1653
2501
|
case 'persist-db': {
|
|
1654
2502
|
if (!this.dbClient) {
|
|
1655
2503
|
this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
|
|
@@ -1682,7 +2530,7 @@ _unwrapResolverResult(result) {
|
|
|
1682
2530
|
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
1683
2531
|
await db.collection(step.collection).insertOne({
|
|
1684
2532
|
workflow_name: this.context.workflow_name || 'unknown',
|
|
1685
|
-
|
|
2533
|
+
sourceValue,
|
|
1686
2534
|
created_at: new Date()
|
|
1687
2535
|
});
|
|
1688
2536
|
break;
|
|
@@ -1717,18 +2565,20 @@ _unwrapResolverResult(result) {
|
|
|
1717
2565
|
if (workflow.type !== 'workflow') {
|
|
1718
2566
|
throw new Error(`Unknown workflow type: ${workflow.type}`);
|
|
1719
2567
|
}
|
|
1720
|
-
|
|
2568
|
+
|
|
1721
2569
|
this.context = {
|
|
1722
2570
|
...inputs,
|
|
1723
2571
|
workflow_name: workflow.name
|
|
1724
2572
|
};
|
|
1725
2573
|
|
|
1726
|
-
|
|
1727
|
-
|
|
2574
|
+
// ✅ NEW: Validate Inputs BEFORE any step runs
|
|
2575
|
+
// In redact mode: PII tokens are replaced in place, execution continues.
|
|
2576
|
+
// In block mode (default): throws on any PII or financial intent match.
|
|
2577
|
+
this._validateInputs(inputs);
|
|
1728
2578
|
|
|
1729
2579
|
// ✅ AUDIT LOG: Workflow start (ENHANCED with governance metadata)
|
|
1730
2580
|
const governanceHash = this._generateGovernanceProfileHash(workflow);
|
|
1731
|
-
|
|
2581
|
+
|
|
1732
2582
|
this._createAuditEntry('workflow_started', {
|
|
1733
2583
|
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`, // ✅ Workflow ID
|
|
1734
2584
|
workflow_name: workflow.name,
|
|
@@ -1739,6 +2589,7 @@ _unwrapResolverResult(result) {
|
|
|
1739
2589
|
inputs_count: Object.keys(inputs).length,
|
|
1740
2590
|
steps_count: workflow.steps.length,
|
|
1741
2591
|
allowed_resolvers: workflow.allowedResolvers || [],
|
|
2592
|
+
pii_mode: this.piiMode, // ✅ NEW v1.3.0 — surfaced in audit
|
|
1742
2593
|
constraints: {
|
|
1743
2594
|
max_generations: workflow.maxGenerations,
|
|
1744
2595
|
strict_inputs: process.env.OLANG_STRICT_INPUTS === 'true'
|
|
@@ -1773,7 +2624,7 @@ _unwrapResolverResult(result) {
|
|
|
1773
2624
|
for (const step of workflow.steps) {
|
|
1774
2625
|
await this.executeStep(step, agentResolver);
|
|
1775
2626
|
}
|
|
1776
|
-
|
|
2627
|
+
|
|
1777
2628
|
// ✅ AUDIT LOG: Workflow completion (ENHANCED)
|
|
1778
2629
|
this._createAuditEntry('workflow_completed', {
|
|
1779
2630
|
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`,
|