@o-lang/olang 1.2.13 → 1.2.15
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 +1 -1
- package/src/parser/index.js +24 -27
- package/src/runtime/RuntimeAPI.js +775 -471
- package/src/runtime/index.js +2 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const crypto = require('crypto'); // ✅ CRYPTOGRAPHIC AUDIT LOGS
|
|
4
|
+
|
|
5
|
+
// ✅ O-Lang Kernel Version (Safety Logic & Governance Rules)
|
|
6
|
+
const KERNEL_VERSION = '1.2.15-alpha'; // 🔁 Update when safety rules change
|
|
3
7
|
|
|
4
8
|
class RuntimeAPI {
|
|
5
9
|
constructor({ verbose = false } = {}) {
|
|
6
|
-
|
|
10
|
+
// console.log('✅ KERNEL FIX VERIFIED - Unwrapping active');
|
|
7
11
|
this.context = {};
|
|
8
12
|
this.resources = {};
|
|
9
13
|
this.agentMap = {};
|
|
@@ -12,23 +16,31 @@ class RuntimeAPI {
|
|
|
12
16
|
this.allowedResolvers = new Set();
|
|
13
17
|
this.verbose = verbose;
|
|
14
18
|
this.__warnings = [];
|
|
15
|
-
|
|
16
19
|
const logsDir = path.resolve('./logs');
|
|
17
20
|
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
|
18
21
|
this.disallowedLogFile = path.join(logsDir, 'disallowed_resolvers.json');
|
|
19
22
|
this.disallowedAttempts = [];
|
|
20
|
-
|
|
23
|
+
|
|
21
24
|
// ✅ NEW: Database client setup
|
|
22
25
|
this.dbClient = null;
|
|
23
26
|
this._initDbClient();
|
|
27
|
+
|
|
28
|
+
// ✅ NEW: Cryptographically verifiable audit logs
|
|
29
|
+
this.auditLog = [];
|
|
30
|
+
this.previousHash = 'GENESIS';
|
|
31
|
+
this.auditLogPrivateKey = process.env.OLANG_AUDIT_PRIVATE_KEY;
|
|
32
|
+
this.auditLogFile = path.join(logsDir, 'audit_log.json');
|
|
33
|
+
this.enableAuditLog = process.env.OLANG_AUDIT_LOG === 'true';
|
|
34
|
+
|
|
35
|
+
if (this.enableAuditLog && this.verbose) {
|
|
36
|
+
console.log('🔐 Cryptographically verifiable audit logging enabled');
|
|
37
|
+
}
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
// ✅ NEW: Initialize database client
|
|
27
41
|
_initDbClient() {
|
|
28
42
|
const dbType = process.env.OLANG_DB_TYPE; // 'postgres', 'mysql', 'mongodb', 'sqlite'
|
|
29
|
-
|
|
30
43
|
if (!dbType) return; // DB persistence disabled
|
|
31
|
-
|
|
32
44
|
try {
|
|
33
45
|
switch (dbType.toLowerCase()) {
|
|
34
46
|
case 'postgres':
|
|
@@ -45,7 +57,6 @@ class RuntimeAPI {
|
|
|
45
57
|
})
|
|
46
58
|
};
|
|
47
59
|
break;
|
|
48
|
-
|
|
49
60
|
case 'mysql':
|
|
50
61
|
const mysql = require('mysql2/promise');
|
|
51
62
|
this.dbClient = {
|
|
@@ -59,7 +70,6 @@ class RuntimeAPI {
|
|
|
59
70
|
})
|
|
60
71
|
};
|
|
61
72
|
break;
|
|
62
|
-
|
|
63
73
|
case 'mongodb':
|
|
64
74
|
const { MongoClient } = require('mongodb');
|
|
65
75
|
const uri = process.env.MONGO_URI || `mongodb://${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || 27017}`;
|
|
@@ -68,7 +78,6 @@ class RuntimeAPI {
|
|
|
68
78
|
client: new MongoClient(uri)
|
|
69
79
|
};
|
|
70
80
|
break;
|
|
71
|
-
|
|
72
81
|
case 'sqlite':
|
|
73
82
|
const Database = require('better-sqlite3');
|
|
74
83
|
const dbPath = process.env.SQLITE_PATH || './olang.db';
|
|
@@ -79,11 +88,9 @@ class RuntimeAPI {
|
|
|
79
88
|
client: new Database(dbPath)
|
|
80
89
|
};
|
|
81
90
|
break;
|
|
82
|
-
|
|
83
91
|
default:
|
|
84
92
|
throw new Error(`Unsupported database type: ${dbType}`);
|
|
85
93
|
}
|
|
86
|
-
|
|
87
94
|
if (this.verbose) {
|
|
88
95
|
console.log(`🗄️ Database client initialized: ${dbType}`);
|
|
89
96
|
}
|
|
@@ -93,6 +100,391 @@ class RuntimeAPI {
|
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
// ================================
|
|
104
|
+
// ✅ CRYPTOGRAPHIC AUDIT LOG METHODS
|
|
105
|
+
// ================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a hash of data using SHA-256
|
|
109
|
+
*/
|
|
110
|
+
_hash(data) {
|
|
111
|
+
return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sign data with private key (if available)
|
|
116
|
+
*/
|
|
117
|
+
_sign(data) {
|
|
118
|
+
if (!this.auditLogPrivateKey) return null;
|
|
119
|
+
try {
|
|
120
|
+
const sign = crypto.createSign('SHA256');
|
|
121
|
+
sign.update(data);
|
|
122
|
+
sign.end();
|
|
123
|
+
return sign.sign(this.auditLogPrivateKey, 'base64');
|
|
124
|
+
} catch (e) {
|
|
125
|
+
this.addWarning(`Audit log signing failed: ${e.message}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a cryptographically verifiable audit log entry
|
|
132
|
+
*/
|
|
133
|
+
_createAuditEntry(event, details, context = {}) {
|
|
134
|
+
const timestamp = new Date().toISOString();
|
|
135
|
+
const entryData = {
|
|
136
|
+
timestamp,
|
|
137
|
+
event,
|
|
138
|
+
details,
|
|
139
|
+
workflow: this.context.workflow_name,
|
|
140
|
+
contextSnapshot: this._captureContextSnapshot(context),
|
|
141
|
+
previousHash: this.previousHash,
|
|
142
|
+
sequenceNumber: this.auditLog.length + 1
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Create hash of this entry
|
|
146
|
+
const entryHash = this._hash(entryData);
|
|
147
|
+
|
|
148
|
+
// Sign the entry if private key available
|
|
149
|
+
const signature = this._sign(entryHash);
|
|
150
|
+
|
|
151
|
+
const entry = {
|
|
152
|
+
...entryData,
|
|
153
|
+
hash: entryHash,
|
|
154
|
+
signature,
|
|
155
|
+
publicKey: this.auditLogPrivateKey ?
|
|
156
|
+
crypto.createPublicKey(this.auditLogPrivateKey).export({
|
|
157
|
+
type: 'spki',
|
|
158
|
+
format: 'pem'
|
|
159
|
+
}) : null
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Update chain
|
|
163
|
+
this.previousHash = entryHash;
|
|
164
|
+
this.auditLog.push(entry);
|
|
165
|
+
|
|
166
|
+
// Persist to file if enabled
|
|
167
|
+
if (this.enableAuditLog) {
|
|
168
|
+
this._persistAuditLog();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return entry;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Capture relevant context snapshot for audit
|
|
176
|
+
*/
|
|
177
|
+
_captureContextSnapshot(keys = []) {
|
|
178
|
+
const snapshot = {};
|
|
179
|
+
const keysToCapture = keys.length > 0 ? keys : [
|
|
180
|
+
'workflow_name',
|
|
181
|
+
'current_step',
|
|
182
|
+
'agent_id'
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
for (const key of keysToCapture) {
|
|
186
|
+
if (this.context[key] !== undefined) {
|
|
187
|
+
snapshot[key] = this.context[key];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return snapshot;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Persist audit log to file
|
|
196
|
+
*/
|
|
197
|
+
_persistAuditLog() {
|
|
198
|
+
try {
|
|
199
|
+
fs.writeFileSync(
|
|
200
|
+
this.auditLogFile,
|
|
201
|
+
JSON.stringify(this.auditLog, null, 2),
|
|
202
|
+
'utf8'
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Also persist to DB if configured
|
|
206
|
+
if (this.dbClient && process.env.OLANG_AUDIT_DB_PERSIST === 'true') {
|
|
207
|
+
this._persistAuditLogToDB();
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
this.addWarning(`Failed to persist audit log: ${e.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Persist audit log to database
|
|
216
|
+
*/
|
|
217
|
+
async _persistAuditLogToDB() {
|
|
218
|
+
try {
|
|
219
|
+
const latestEntry = this.auditLog[this.auditLog.length - 1];
|
|
220
|
+
if (!latestEntry) return;
|
|
221
|
+
|
|
222
|
+
switch (this.dbClient.type) {
|
|
223
|
+
case 'postgres':
|
|
224
|
+
await this.dbClient.client.query(
|
|
225
|
+
`INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
|
|
226
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
227
|
+
[
|
|
228
|
+
latestEntry.hash,
|
|
229
|
+
latestEntry.previousHash,
|
|
230
|
+
latestEntry.event,
|
|
231
|
+
JSON.stringify(latestEntry.details),
|
|
232
|
+
latestEntry.timestamp,
|
|
233
|
+
latestEntry.workflow,
|
|
234
|
+
latestEntry.signature,
|
|
235
|
+
latestEntry.sequenceNumber
|
|
236
|
+
]
|
|
237
|
+
);
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'mysql':
|
|
241
|
+
await this.dbClient.client.execute(
|
|
242
|
+
`INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
|
|
243
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
244
|
+
[
|
|
245
|
+
latestEntry.hash,
|
|
246
|
+
latestEntry.previousHash,
|
|
247
|
+
latestEntry.event,
|
|
248
|
+
JSON.stringify(latestEntry.details),
|
|
249
|
+
latestEntry.timestamp,
|
|
250
|
+
latestEntry.workflow,
|
|
251
|
+
latestEntry.signature,
|
|
252
|
+
latestEntry.sequenceNumber
|
|
253
|
+
]
|
|
254
|
+
);
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
case 'mongodb':
|
|
258
|
+
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
259
|
+
await db.collection('audit_log').insertOne({
|
|
260
|
+
hash: latestEntry.hash,
|
|
261
|
+
previous_hash: latestEntry.previousHash,
|
|
262
|
+
event: latestEntry.event,
|
|
263
|
+
details: latestEntry.details,
|
|
264
|
+
timestamp: new Date(latestEntry.timestamp),
|
|
265
|
+
workflow_name: latestEntry.workflow,
|
|
266
|
+
signature: latestEntry.signature,
|
|
267
|
+
sequence_number: latestEntry.sequenceNumber
|
|
268
|
+
});
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case 'sqlite':
|
|
272
|
+
const stmt = this.dbClient.client.prepare(
|
|
273
|
+
`INSERT INTO audit_log (hash, previous_hash, event, details, timestamp, workflow_name, signature, sequence_number)
|
|
274
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
275
|
+
);
|
|
276
|
+
stmt.run(
|
|
277
|
+
latestEntry.hash,
|
|
278
|
+
latestEntry.previousHash,
|
|
279
|
+
latestEntry.event,
|
|
280
|
+
JSON.stringify(latestEntry.details),
|
|
281
|
+
latestEntry.timestamp,
|
|
282
|
+
latestEntry.workflow,
|
|
283
|
+
latestEntry.signature,
|
|
284
|
+
latestEntry.sequenceNumber
|
|
285
|
+
);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
this.addWarning(`Failed to persist audit log to DB: ${e.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Verify the integrity of the audit log chain
|
|
295
|
+
*/
|
|
296
|
+
verifyAuditLogIntegrity(auditLog = null) {
|
|
297
|
+
const log = auditLog || this.auditLog;
|
|
298
|
+
if (log.length === 0) {
|
|
299
|
+
return { valid: true, message: 'Audit log is empty' };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Verify genesis block
|
|
303
|
+
if (log[0].previousHash !== 'GENESIS') {
|
|
304
|
+
return { valid: false, error: 'Invalid genesis block', failedAtIndex: 0 };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Verify chain integrity
|
|
308
|
+
let previousHash = 'GENESIS';
|
|
309
|
+
for (let i = 0; i < log.length; i++) {
|
|
310
|
+
const entry = log[i];
|
|
311
|
+
|
|
312
|
+
// Check previous hash linkage
|
|
313
|
+
if (entry.previousHash !== previousHash) {
|
|
314
|
+
return {
|
|
315
|
+
valid: false,
|
|
316
|
+
error: `Hash chain broken at entry ${i}`,
|
|
317
|
+
failedAtIndex: i,
|
|
318
|
+
expected: previousHash,
|
|
319
|
+
actual: entry.previousHash
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Verify entry hash
|
|
324
|
+
const entryData = {
|
|
325
|
+
timestamp: entry.timestamp,
|
|
326
|
+
event: entry.event,
|
|
327
|
+
details: entry.details,
|
|
328
|
+
workflow: entry.workflow,
|
|
329
|
+
contextSnapshot: entry.contextSnapshot,
|
|
330
|
+
previousHash: entry.previousHash,
|
|
331
|
+
sequenceNumber: entry.sequenceNumber
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const calculatedHash = this._hash(entryData);
|
|
335
|
+
if (calculatedHash !== entry.hash) {
|
|
336
|
+
return {
|
|
337
|
+
valid: false,
|
|
338
|
+
error: `Entry hash mismatch at index ${i}`,
|
|
339
|
+
failedAtIndex: i,
|
|
340
|
+
expected: calculatedHash,
|
|
341
|
+
actual: entry.hash
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Verify signature if present
|
|
346
|
+
if (entry.signature && entry.publicKey) {
|
|
347
|
+
try {
|
|
348
|
+
const verify = crypto.createVerify('SHA256');
|
|
349
|
+
verify.update(entry.hash);
|
|
350
|
+
verify.end();
|
|
351
|
+
const isValid = verify.verify(entry.publicKey, entry.signature, 'base64');
|
|
352
|
+
if (!isValid) {
|
|
353
|
+
return {
|
|
354
|
+
valid: false,
|
|
355
|
+
error: `Signature verification failed at entry ${i}`,
|
|
356
|
+
failedAtIndex: i
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
} catch (e) {
|
|
360
|
+
return {
|
|
361
|
+
valid: false,
|
|
362
|
+
error: `Signature verification error at entry ${i}: ${e.message}`,
|
|
363
|
+
failedAtIndex: i
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
previousHash = entry.hash;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
valid: true,
|
|
373
|
+
message: `Audit log verified successfully (${log.length} entries)`,
|
|
374
|
+
totalEntries: log.length,
|
|
375
|
+
lastHash: previousHash
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get audit log excerpt with proof
|
|
381
|
+
*/
|
|
382
|
+
getAuditExcerpt(startIndex, endIndex) {
|
|
383
|
+
const excerpt = this.auditLog.slice(startIndex, endIndex + 1);
|
|
384
|
+
const proof = {
|
|
385
|
+
excerpt,
|
|
386
|
+
previousHash: startIndex > 0 ? this.auditLog[startIndex - 1].hash : 'GENESIS',
|
|
387
|
+
nextHash: endIndex < this.auditLog.length - 1 ? this.auditLog[endIndex + 1].hash : null,
|
|
388
|
+
totalEntries: this.auditLog.length
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
return proof;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Export audit log with verification data
|
|
396
|
+
*/
|
|
397
|
+
exportAuditLog() {
|
|
398
|
+
const verification = this.verifyAuditLogIntegrity();
|
|
399
|
+
return {
|
|
400
|
+
auditLog: this.auditLog,
|
|
401
|
+
verification,
|
|
402
|
+
exportedAt: new Date().toISOString(),
|
|
403
|
+
totalEntries: this.auditLog.length,
|
|
404
|
+
merkleRoot: this._calculateMerkleRoot()
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Calculate Merkle root of audit log
|
|
410
|
+
*/
|
|
411
|
+
_calculateMerkleRoot() {
|
|
412
|
+
if (this.auditLog.length === 0) return null;
|
|
413
|
+
|
|
414
|
+
let hashes = this.auditLog.map(entry => entry.hash);
|
|
415
|
+
|
|
416
|
+
while (hashes.length > 1) {
|
|
417
|
+
const newLevel = [];
|
|
418
|
+
for (let i = 0; i < hashes.length; i += 2) {
|
|
419
|
+
const left = hashes[i];
|
|
420
|
+
const right = i + 1 < hashes.length ? hashes[i + 1] : left;
|
|
421
|
+
newLevel.push(this._hash(left + right));
|
|
422
|
+
}
|
|
423
|
+
hashes = newLevel;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return hashes[0];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get audit summary
|
|
431
|
+
*/
|
|
432
|
+
getAuditSummary() {
|
|
433
|
+
const verification = this.verifyAuditLogIntegrity();
|
|
434
|
+
return {
|
|
435
|
+
totalEntries: this.auditLog.length,
|
|
436
|
+
integrity: verification,
|
|
437
|
+
events: this.auditLog.map(e => e.event),
|
|
438
|
+
merkleRoot: this._calculateMerkleRoot()
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ================================
|
|
443
|
+
// ✅ GOVERNANCE METADATA HELPERS
|
|
444
|
+
// ================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Generate immutable hash of governance profile
|
|
448
|
+
* Includes: allowed resolvers, constraints, policy flags
|
|
449
|
+
*/
|
|
450
|
+
_generateGovernanceProfileHash(workflow) {
|
|
451
|
+
const profile = {
|
|
452
|
+
version: '1.0',
|
|
453
|
+
allowedResolvers: Array.from(this.allowedResolvers).sort(),
|
|
454
|
+
maxGenerations: workflow.maxGenerations,
|
|
455
|
+
strictInputs: process.env.OLANG_STRICT_INPUTS === 'true',
|
|
456
|
+
semanticValidation: true,
|
|
457
|
+
hallucinationPrevention: true,
|
|
458
|
+
resolverPolicy: 'allowlist-only',
|
|
459
|
+
timestamp: new Date().toISOString()
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
return crypto.createHash('sha256')
|
|
463
|
+
.update(JSON.stringify(profile))
|
|
464
|
+
.digest('hex');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get runtime metadata for external verification
|
|
469
|
+
*/
|
|
470
|
+
getRuntimeMetadata() {
|
|
471
|
+
return {
|
|
472
|
+
runtime: 'O-Lang Kernel',
|
|
473
|
+
version: KERNEL_VERSION,
|
|
474
|
+
features: {
|
|
475
|
+
semanticValidation: true,
|
|
476
|
+
hallucinationPrevention: true,
|
|
477
|
+
cryptographicAudit: true,
|
|
478
|
+
multiDatabaseSupport: true
|
|
479
|
+
},
|
|
480
|
+
environment: {
|
|
481
|
+
auditEnabled: this.enableAuditLog,
|
|
482
|
+
dbType: this.dbClient?.type || 'none',
|
|
483
|
+
strictMode: process.env.OLANG_STRICT_INPUTS === 'true'
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
96
488
|
// -----------------------------
|
|
97
489
|
// ✅ SEMANTIC ENFORCEMENT HELPER
|
|
98
490
|
// -----------------------------
|
|
@@ -106,15 +498,12 @@ class RuntimeAPI {
|
|
|
106
498
|
used_by: stepType,
|
|
107
499
|
phase: 'execution'
|
|
108
500
|
};
|
|
109
|
-
|
|
110
501
|
// Emit semantic event (for observability)
|
|
111
502
|
this.emit('semantic_violation', error);
|
|
112
|
-
|
|
113
503
|
// Log as error (not warning)
|
|
114
504
|
if (this.verbose) {
|
|
115
505
|
console.error(`[O-Lang SEMANTIC] Missing required symbol "${symbol}" for ${stepType}`);
|
|
116
506
|
}
|
|
117
|
-
|
|
118
507
|
return false;
|
|
119
508
|
}
|
|
120
509
|
return true;
|
|
@@ -154,7 +543,20 @@ class RuntimeAPI {
|
|
|
154
543
|
const entry = { resolver: resolverName, step: stepAction, timestamp: new Date().toISOString() };
|
|
155
544
|
fs.appendFileSync(this.disallowedLogFile, JSON.stringify(entry) + '\n', 'utf8');
|
|
156
545
|
this.disallowedAttempts.push(entry);
|
|
157
|
-
|
|
546
|
+
|
|
547
|
+
// ✅ AUDIT LOG: Security violation with governance context
|
|
548
|
+
this._createAuditEntry('security_violation', {
|
|
549
|
+
type: 'disallowed_resolver',
|
|
550
|
+
resolver: resolverName,
|
|
551
|
+
step: stepAction,
|
|
552
|
+
severity: 'high',
|
|
553
|
+
kernel_version: KERNEL_VERSION,
|
|
554
|
+
governance_profile_hash: this._generateGovernanceProfileHash({
|
|
555
|
+
allowedResolvers: Array.from(this.allowedResolvers),
|
|
556
|
+
maxGenerations: null
|
|
557
|
+
})
|
|
558
|
+
});
|
|
559
|
+
|
|
158
560
|
if (this.verbose) {
|
|
159
561
|
console.warn(`[O-Lang] Disallowed resolver blocked: ${resolverName} | step: ${stepAction}`);
|
|
160
562
|
}
|
|
@@ -193,7 +595,6 @@ class RuntimeAPI {
|
|
|
193
595
|
const manifest = resolver.manifest;
|
|
194
596
|
const endpoint = manifest.endpoint;
|
|
195
597
|
const timeoutMs = manifest.timeout_ms || 30000;
|
|
196
|
-
|
|
197
598
|
const payload = {
|
|
198
599
|
action,
|
|
199
600
|
context,
|
|
@@ -204,7 +605,6 @@ class RuntimeAPI {
|
|
|
204
605
|
|
|
205
606
|
const controller = new AbortController();
|
|
206
607
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
207
|
-
|
|
208
608
|
try {
|
|
209
609
|
const res = await fetch(`${endpoint}/resolve`, {
|
|
210
610
|
method: 'POST',
|
|
@@ -212,17 +612,13 @@ class RuntimeAPI {
|
|
|
212
612
|
body: JSON.stringify(payload),
|
|
213
613
|
signal: controller.signal
|
|
214
614
|
});
|
|
215
|
-
|
|
216
615
|
if (!res.ok) {
|
|
217
616
|
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
218
617
|
}
|
|
219
|
-
|
|
220
618
|
const json = await res.json();
|
|
221
|
-
|
|
222
619
|
if (json?.error) {
|
|
223
620
|
throw new Error(json.error.message || 'External resolver error');
|
|
224
621
|
}
|
|
225
|
-
|
|
226
622
|
return json.result;
|
|
227
623
|
} catch (err) {
|
|
228
624
|
if (err.name === 'AbortError') {
|
|
@@ -279,11 +675,9 @@ class RuntimeAPI {
|
|
|
279
675
|
if (typeof value === 'string') return `"${value.replace(/"/g, '\\"')}"`;
|
|
280
676
|
return value !== undefined ? value : 0;
|
|
281
677
|
});
|
|
282
|
-
|
|
283
678
|
const funcNames = Object.keys(this.mathFunctions);
|
|
284
679
|
const safeFunc = {};
|
|
285
680
|
funcNames.forEach(fn => safeFunc[fn] = this.mathFunctions[fn]);
|
|
286
|
-
|
|
287
681
|
try {
|
|
288
682
|
const f = new Function(...funcNames, `return ${expr};`);
|
|
289
683
|
return f(...funcNames.map(fn => safeFunc[fn]));
|
|
@@ -307,18 +701,15 @@ class RuntimeAPI {
|
|
|
307
701
|
_safeInterpolate(template, context, contextType = 'action') {
|
|
308
702
|
return template.replace(/\{([^\}]+)\}/g, (_, path) => {
|
|
309
703
|
const value = this.getNested(context, path.trim());
|
|
310
|
-
|
|
311
704
|
if (value === undefined) {
|
|
312
705
|
return `{${path}}`; // Preserve placeholder for downstream validation
|
|
313
706
|
}
|
|
314
|
-
|
|
315
707
|
// 🔒 SAFETY GUARD: Block object/array interpolation into string contexts
|
|
316
708
|
if (value !== null && typeof value === 'object') {
|
|
317
709
|
const type = Array.isArray(value) ? 'array' : 'object';
|
|
318
|
-
const keys = Object.keys(value).length > 0
|
|
319
|
-
? Object.keys(value).join(', ')
|
|
710
|
+
const keys = Object.keys(value).length > 0
|
|
711
|
+
? Object.keys(value).join(', ')
|
|
320
712
|
: '(empty)';
|
|
321
|
-
|
|
322
713
|
throw new Error(
|
|
323
714
|
`[O-Lang SAFETY] Cannot interpolate ${type} "{${path}}" into ${contextType}.\n` +
|
|
324
715
|
` → Contains fields: ${keys}\n` +
|
|
@@ -326,217 +717,200 @@ class RuntimeAPI {
|
|
|
326
717
|
`\n🛑 Halting to prevent data corruption → LLM hallucination.`
|
|
327
718
|
);
|
|
328
719
|
}
|
|
329
|
-
|
|
330
720
|
return String(value);
|
|
331
721
|
});
|
|
332
722
|
}
|
|
333
723
|
|
|
334
|
-
// -----------------------------
|
|
335
|
-
// ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION (CONJUGATION-AWARE + EVASION-RESISTANT)
|
|
336
|
-
// -----------------------------
|
|
337
|
-
_validateLLMOutput(output, actionContext) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// 🔑 Extract allowed capabilities from workflow allowlist
|
|
341
|
-
const allowedCapabilities = Array.from(this.allowedResolvers)
|
|
342
|
-
.filter(name => !name.startsWith('llm-') && name !== 'builtInMathResolver')
|
|
343
|
-
.map(name => name.replace('@o-lang/', '').replace(/-resolver$/, ''));
|
|
344
|
-
|
|
345
|
-
// 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION
|
|
346
|
-
const forbiddenPatterns = [
|
|
347
|
-
// ────────────────────────────────────────────────
|
|
348
|
-
// 🇳🇬 NIGERIAN LANGUAGES (Conjugation-aware)
|
|
349
|
-
// ────────────────────────────────────────────────
|
|
350
|
-
|
|
351
|
-
// Yoruba (yo) - Perfective "ti" + Progressive "ń/ǹ/n"
|
|
352
|
-
{ pattern: /\bti\s+(?:fi|san|gba|da|lo)\b/i, capability: 'unauthorized_action', lang: 'yo' }, // "has transferred/paid/withdrawn"
|
|
353
|
-
{ pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)\b/i, capability: 'unauthorized_action', lang: 'yo' }, // Progressive "is transferring/paying"
|
|
354
|
-
{ pattern: /\b(fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ))\b/i, capability: 'transfer', lang: 'yo' },
|
|
355
|
-
{ pattern: /\b(san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo))\b/i, capability: 'payment', lang: 'yo' },
|
|
356
|
-
{ pattern: /\b(gba\s+owo)\b/i, capability: 'withdrawal', lang: 'yo' },
|
|
357
|
-
{ pattern: /\b(mo\s+ti\s+(?:fi|san|gba))\b/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
358
|
-
|
|
359
|
-
// Hausa (ha) - Perfective "ya/ta/su" + Future "za a/za ta"
|
|
360
|
-
{ pattern: /\b(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)\b/i, capability: 'unauthorized_action', lang: 'ha' }, // "he/she/they transferred/paid/withdrew/deposited"
|
|
361
|
-
{ pattern: /\b(?:za\sa|za\s+ta)\s+(?:ciyar|biya)\b/i, capability: 'unauthorized_action', lang: 'ha' }, // Future "will transfer/pay"
|
|
362
|
-
{ pattern: /\b(ciyar\s*(?:da)?|ciya\s*(?:da)?|shiga\s+kuɗi)\b/i, capability: 'transfer', lang: 'ha' },
|
|
363
|
-
{ pattern: /\b(biya\s*(?:da)?)\b/i, capability: 'payment', lang: 'ha' },
|
|
364
|
-
{ pattern: /\b(sahaw[ae]\s+kuɗi)\b/i, capability: 'withdrawal', lang: 'ha' },
|
|
365
|
-
{ pattern: /\b(ina\s+(?:ciyar|biya|sahawa))\b/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
366
|
-
|
|
367
|
-
// Igbo (ig) - Perfective suffixes
|
|
368
|
-
{ pattern: /\b(?:ziri|bururu|tinyere|gbara)\b/i, capability: 'unauthorized_action', lang: 'ig' }, // "has sent/carried/deposited/withdrawn"
|
|
369
|
-
{ pattern: /\b(zipu\s+(?:ego|moni|isi|na))\b/i, capability: 'transfer', lang: 'ig' },
|
|
370
|
-
{ pattern: /\b(buru\s+(?:ego|moni|isi))\b/i, capability: 'transfer', lang: 'ig' },
|
|
371
|
-
{ pattern: /\b(tinye\s+(?:ego|moni|isi))\b/i, capability: 'deposit', lang: 'ig' },
|
|
372
|
-
{ pattern: /\b(m\s+(?:ziri|buru|zipuru|tinyere))\b/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
373
|
-
|
|
374
|
-
// ────────────────────────────────────────────────
|
|
375
|
-
// 🌍 PAN-AFRICAN LANGUAGES (Conjugation-aware + Evasion-resistant)
|
|
376
|
-
// ────────────────────────────────────────────────
|
|
377
|
-
|
|
378
|
-
// Swahili (sw) - ALL ASPECTS: Perfect, Continuous Passive, Future
|
|
379
|
-
{ pattern: /\b(?:ni|u|a|tu|m|wa|ki|vi|zi|i)\s*me\s*(?:ongeza|weka|tuma|peleka|lipa|wasilisha)\b/i, capability: 'unauthorized_action', lang: 'sw' }, // Perfect: "nimeongeza" (I have added)
|
|
380
|
-
{ pattern: /\b(?:kime|lime|ime|ume|nime|vime|zyme|yame|mame)(?:ongezwa|wekwa|tumwa|pelekwa|lipwa|wasilishwa|fanyika)\b/i, capability: 'unauthorized_action', lang: 'sw' }, // Passive perfect: "kimeongezwa" (has been added)
|
|
381
|
-
{ pattern: /\b(?:ki|vi|mi|ma|u|wa|i|zi|ya|li|tu|mu|a|pa|ku)na(?:cho|vyo|yo|lo|mo|o)?(?:tum|pelek|wasil|ongez|wek|lip)\w*wa\b/i, capability: 'unauthorized_action', lang: 'sw' }, // Continuous passive: "kinachowasilishwa" (is being delivered) ← CRITICAL FIX
|
|
382
|
-
{ pattern: /\b(?:ki|vi|mi|ma|u|wa|i|zi|ya|li|tu|mu|a|pa|ku)ta(?:tum|pelek|wasil|ongez|wek|lip)\w*\b/i, capability: 'unauthorized_action', lang: 'sw' }, // Future: "kitatuma" (will send)
|
|
383
|
-
{ pattern: /\b(tuma\s+(?:pesa|fedha)|pelek[ae]?\s+(?:pesa|fedha)|wasilisha)\b/i, capability: 'transfer', lang: 'sw' },
|
|
384
|
-
{ pattern: /\b(lipa|maliza\s+malipo)\b/i, capability: 'payment', lang: 'sw' },
|
|
385
|
-
{ pattern: /\b(ongez[ae]?\s*(?:kiasi|pesa|fedha)|wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa))\b/i, capability: 'deposit', lang: 'sw' },
|
|
386
|
-
{ pattern: /\b(nime(?:tuma|lipa|ongeza|weka|peleka))\b/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
387
|
-
|
|
388
|
-
// Amharic (am) - Perfective suffix (Ethiopic script)
|
|
389
|
-
{ pattern: /[\u1200-\u137F]{0,4}(?:ተላላፈ|ላክ|ክፈል|ጨምር|ወጣ|ገባ)[\u1200-\u137F]{0,2}(?:\u1205|\u122d|\u1265)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
|
|
390
|
-
|
|
391
|
-
// Oromo (om) - Perfective "ni...e"
|
|
392
|
-
{ pattern: /\bni\s+(?:kuufe|dhiibe|kennine|gurgure)\b/i, capability: 'unauthorized_action', lang: 'om' },
|
|
393
|
-
{ pattern: /\b(kuuf\s+(?:qilleensaa|bilbila)|dhiib\s+(?:qilleensaa|bilbila))\b/i, capability: 'transfer', lang: 'om' },
|
|
394
|
-
{ pattern: /\b(kenn\s*i|gurgur\s*i)\b/i, capability: 'payment', lang: 'om' },
|
|
395
|
-
|
|
396
|
-
// Fula (ff)
|
|
397
|
-
{ pattern: /\b(sakkit\s+(?:ndo|ndoo)|tawt\s+(?:ndo|ndoo))\b/i, capability: 'transfer', lang: 'ff' },
|
|
398
|
-
{ pattern: /\b(jokk\s*i|soodug\s*i)\b/i, capability: 'payment', lang: 'ff' },
|
|
399
|
-
|
|
400
|
-
// Somali (so) - Perfective "waxaa"
|
|
401
|
-
{ pattern: /\bwaxaa\s+(?:diray|bixiyay|ku\s+daray|sameeyay)\b/i, capability: 'unauthorized_action', lang: 'so' },
|
|
402
|
-
{ pattern: /\b(dir\s+(?:lacag|maal|qarsoon))\b/i, capability: 'transfer', lang: 'so' },
|
|
403
|
-
{ pattern: /\b(bixi|bixis\s*o)\b/i, capability: 'payment', lang: 'so' },
|
|
404
|
-
|
|
405
|
-
// Zulu (zu) - Perfective "-ile"
|
|
406
|
-
{ pattern: /\b(?:thumel|hlawul|fik)\s*ile\b/i, capability: 'unauthorized_action', lang: 'zu' },
|
|
407
|
-
{ pattern: /\b(thumel\s*a\s+(?:imali|imali))\b/i, capability: 'transfer', lang: 'zu' },
|
|
408
|
-
{ pattern: /\b(hlawul\s*a|hlawulel\s*a)\b/i, capability: 'payment', lang: 'zu' },
|
|
409
|
-
{ pattern: /\b(siyithumel\s*e|siyihlawul\s*e)\b/i, capability: 'unauthorized_action', lang: 'zu' },
|
|
410
|
-
|
|
411
|
-
// Shona (sn) - Perfective "-a/-e"
|
|
412
|
-
{ pattern: /\b(?:tumir|bhadhar)\s*a\b/i, capability: 'unauthorized_action', lang: 'sn' },
|
|
413
|
-
{ pattern: /\b(tumir\s*a\s+(?:mhando|ari))\b/i, capability: 'transfer', lang: 'sn' },
|
|
414
|
-
{ pattern: /\b(bhadhara|bhadharis\s*o)\b/i, capability: 'payment', lang: 'sn' },
|
|
415
|
-
|
|
416
|
-
// ────────────────────────────────────────────────
|
|
417
|
-
// 🌐 GLOBAL LANGUAGES (Conjugation-aware)
|
|
418
|
-
// ────────────────────────────────────────────────
|
|
419
|
-
|
|
420
|
-
// English (en) - Perfective + Passive
|
|
421
|
-
{ pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
422
|
-
{ pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
423
|
-
{ pattern: /\b(transfer(?:red|ring)?|send(?:t|ing)?|wire(?:d)?|pay(?:ed|ing)?|withdraw(?:n)?|deposit(?:ed|ing)?|disburse(?:d)?)\b/i, capability: 'financial_action', lang: 'en' },
|
|
424
|
-
{ 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' },
|
|
425
|
-
|
|
426
|
-
// French (fr) - Past participle
|
|
427
|
-
{ 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' },
|
|
428
|
-
{ pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
|
|
429
|
-
|
|
430
|
-
// Arabic (ar) - Perfective past tense
|
|
431
|
-
{ pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}(?:ت|نا|تم|تا|تِ|تُ|تَ)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
|
|
432
|
-
{ pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
|
|
433
|
-
|
|
434
|
-
// Chinese (zh) - Perfective "le" particle
|
|
435
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|支付|存款|取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
436
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账|转帐|支付|付款|提款|取款|存款|存入|汇款|存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
437
|
-
{ pattern: /[\u4e00-\u9fff]{0,2}(?:我|已|已经)\s*(?:转账|支付|提款|存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
|
|
438
|
-
|
|
439
|
-
// ────────────────────────────────────────────────
|
|
440
|
-
// 🛡️ EVASION-RESISTANT NUMERIC DECEPTION (Catches obfuscated claims)
|
|
441
|
-
// ────────────────────────────────────────────────
|
|
442
|
-
|
|
443
|
-
// Number + account reference within 40 chars (catches "10,000 ... account 123")
|
|
444
|
-
{
|
|
445
|
-
pattern: /(?:^|\s|[:\(\[])(?:\d{1,3}(?:[,\s.]\d{3})*(?:[.,]\d{1,2})?|\d+(?:[.,]\d{1,2})?)(?:\s*(?:naira|ngn|₦|\$|usd|kes|tzs|ugx|rwf|cdf|xof|xaf|ghs|zar))?.{0,40}(?:account|acct|a\/c|akaunti|asusu|akwụkwọ\s+ọkụ|hesabu|namba|#)\b/i,
|
|
446
|
-
capability: 'unauthorized_action',
|
|
447
|
-
lang: 'multi'
|
|
448
|
-
},
|
|
449
|
-
|
|
450
|
-
// ────────────────────────────────────────────────
|
|
451
|
-
// 🔒 PII LEAKAGE PATTERNS
|
|
452
|
-
// ────────────────────────────────────────────────
|
|
453
|
-
|
|
454
|
-
// Account numbers (6+ digits)
|
|
455
|
-
{ pattern: /\b(?:account|acct|a\/c|akaunti|asusu|akwụkwọ\s+ọkụ|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})\b/i, capability: 'pii_exposure', lang: 'multi' },
|
|
456
|
-
|
|
457
|
-
// Nigerian BVN (11 digits)
|
|
458
|
-
{ pattern: /\b(?:bvn|bank verification number)\s*[:\-]?\s*(\d{11})\b/i, capability: 'pii_exposure', lang: 'multi' },
|
|
459
|
-
|
|
460
|
-
// Nigerian phone numbers
|
|
461
|
-
{ pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
|
|
462
|
-
|
|
463
|
-
// ────────────────────────────────────────────────
|
|
464
|
-
// ✅ FAKE CONFIRMATION PATTERNS
|
|
465
|
-
// ────────────────────────────────────────────────
|
|
724
|
+
// -----------------------------
|
|
725
|
+
// ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION (CONJUGATION-AWARE + EVASION-RESISTANT)
|
|
726
|
+
// -----------------------------
|
|
727
|
+
_validateLLMOutput(output, actionContext) {
|
|
728
|
+
if (!output || typeof output !== 'string') return { passed: true };
|
|
466
729
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
)
|
|
730
|
+
// 🔑 Extract allowed capabilities from workflow allowlist
|
|
731
|
+
const allowedCapabilities = Array.from(this.allowedResolvers)
|
|
732
|
+
.filter(name => !name.startsWith('llm-') && name !== 'builtInMathResolver')
|
|
733
|
+
.map(name => name.replace('@o-lang/', '').replace(/-resolver$/, ''));
|
|
734
|
+
|
|
735
|
+
// 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION
|
|
736
|
+
const forbiddenPatterns = [
|
|
737
|
+
// ────────────────────────────────────────────────
|
|
738
|
+
// 🇳🇬 NIGERIAN LANGUAGES (Conjugation-aware)
|
|
739
|
+
// ────────────────────────────────────────────────
|
|
740
|
+
// Yoruba (yo) - Perfective "ti" + Progressive "ń/ǹ/n"
|
|
741
|
+
{ pattern: /\bti\s+(?:fi|san|gba|da|lo)\b/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
742
|
+
{ pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)\b/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
743
|
+
{ pattern: /\b(fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ))\b/i, capability: 'transfer', lang: 'yo' },
|
|
744
|
+
{ pattern: /\b(san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo))\b/i, capability: 'payment', lang: 'yo' },
|
|
745
|
+
{ pattern: /\b(gba\s+owo)\b/i, capability: 'withdrawal', lang: 'yo' },
|
|
746
|
+
{ pattern: /\b(mo\s+ti\s+(?:fi|san|gba))\b/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
482
747
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
748
|
+
// Hausa (ha) - Perfective "ya/ta/su" + Future "za a/za ta"
|
|
749
|
+
{ pattern: /\b(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)\b/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
750
|
+
{ pattern: /\b(?:za\sa|za\s+ta)\s+(?:ciyar|biya)\b/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
751
|
+
{ pattern: /\b(ciyar\s*(?:da)?|ciya\s*(?:da)?|shiga\s+kuɗi)\b/i, capability: 'transfer', lang: 'ha' },
|
|
752
|
+
{ pattern: /\b(biya\s*(?:da)?)\b/i, capability: 'payment', lang: 'ha' },
|
|
753
|
+
{ pattern: /\b(sahaw[ae]\s+kuɗi)\b/i, capability: 'withdrawal', lang: 'ha' },
|
|
754
|
+
{ pattern: /\b(ina\s+(?:ciyar|biya|sahawa))\b/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
755
|
+
|
|
756
|
+
// Igbo (ig) - Perfective suffixes
|
|
757
|
+
{ pattern: /\b(?:ziri|bururu|tinyere|gbara)\b/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
758
|
+
{ pattern: /\b(zipu\s+(?:ego|moni|isi|na))\b/i, capability: 'transfer', lang: 'ig' },
|
|
759
|
+
{ pattern: /\b(buru\s+(?:ego|moni|isi))\b/i, capability: 'transfer', lang: 'ig' },
|
|
760
|
+
{ pattern: /\b(tinye\s+(?:ego|moni|isi))\b/i, capability: 'deposit', lang: 'ig' },
|
|
761
|
+
{ pattern: /\b(m\s+(?:ziri|buru|zipuru|tinyere))\b/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
762
|
+
|
|
763
|
+
// ────────────────────────────────────────────────
|
|
764
|
+
// 🌍 PAN-AFRICAN LANGUAGES (Conjugation-aware + Evasion-resistant)
|
|
765
|
+
// ────────────────────────────────────────────────
|
|
766
|
+
// Swahili (sw) - ALL ASPECTS: Perfect, Continuous Passive, Future
|
|
767
|
+
{ pattern: /\b(?:ni|u|a|tu|m|wa|ki|vi|zi|i)\s*me\s*(?:ongeza|weka|tuma|peleka|lipa|wasilisha)\b/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
768
|
+
{ pattern: /\b(?:kime|lime|ime|ume|nime|vime|zyme|yame|mame)(?:ongezwa|wekwa|tumwa|pelekwa|lipwa|wasilishwa|fanyika)\b/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
769
|
+
{ pattern: /\b(?:ki|vi|mi|ma|u|wa|i|zi|ya|li|tu|mu|a|pa|ku)na(?:cho|vyo|yo|lo|mo|o)?(?:tum|pelek|wasil|ongez|wek|lip)\w*wa\b/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
770
|
+
{ pattern: /\b(?:ki|vi|mi|ma|u|wa|i|zi|ya|li|tu|mu|a|pa|ku)ta(?:tum|pelek|wasil|ongez|wek|lip)\w*\b/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
771
|
+
{ pattern: /\b(tuma\s+(?:pesa|fedha)|pelek[ae]?\s+(?:pesa|fedha)|wasilisha)\b/i, capability: 'transfer', lang: 'sw' },
|
|
772
|
+
{ pattern: /\b(lipa|maliza\s+malipo)\b/i, capability: 'payment', lang: 'sw' },
|
|
773
|
+
{ pattern: /\b(ongez[ae]?\s*(?:kiasi|pesa|fedha)|wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa))\b/i, capability: 'deposit', lang: 'sw' },
|
|
774
|
+
{ pattern: /\b(nime(?:tuma|lipa|ongeza|weka|peleka))\b/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
775
|
+
|
|
776
|
+
// Amharic (am) - Perfective suffix (Ethiopic script)
|
|
777
|
+
{ pattern: /[\u1200-\u137F]{0,4}(?:ተላላፈ|ላክ|ክፈል|ጨምር|ወጣ|ገባ)[\u1200-\u137F]{0,2}(?:\u1205|\u122d|\u1265)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
|
|
778
|
+
|
|
779
|
+
// Oromo (om) - Perfective "ni...e"
|
|
780
|
+
{ pattern: /\bni\s+(?:kuufe|dhiibe|kennine|gurgure)\b/i, capability: 'unauthorized_action', lang: 'om' },
|
|
781
|
+
{ pattern: /\b(kuuf\s+(?:qilleensaa|bilbila)|dhiib\s+(?:qilleensaa|bilbila))\b/i, capability: 'transfer', lang: 'om' },
|
|
782
|
+
{ pattern: /\b(kenn\s*i|gurgur\s*i)\b/i, capability: 'payment', lang: 'om' },
|
|
783
|
+
|
|
784
|
+
// Fula (ff)
|
|
785
|
+
{ pattern: /\b(sakkit\s+(?:ndo|ndoo)|tawt\s+(?:ndo|ndoo))\b/i, capability: 'transfer', lang: 'ff' },
|
|
786
|
+
{ pattern: /\b(jokk\s*i|soodug\s*i)\b/i, capability: 'payment', lang: 'ff' },
|
|
787
|
+
|
|
788
|
+
// Somali (so) - Perfective "waxaa"
|
|
789
|
+
{ pattern: /\bwaxaa\s+(?:diray|bixiyay|ku\s+daray|sameeyay)\b/i, capability: 'unauthorized_action', lang: 'so' },
|
|
790
|
+
{ pattern: /\b(dir\s+(?:lacag|maal|qarsoon))\b/i, capability: 'transfer', lang: 'so' },
|
|
791
|
+
{ pattern: /\b(bixi|bixis\s*o)\b/i, capability: 'payment', lang: 'so' },
|
|
792
|
+
|
|
793
|
+
// Zulu (zu) - Perfective "-ile"
|
|
794
|
+
{ pattern: /\b(?:thumel|hlawul|fik)\s*ile\b/i, capability: 'unauthorized_action', lang: 'zu' },
|
|
795
|
+
{ pattern: /\b(thumel\s*a\s+(?:imali|imali))\b/i, capability: 'transfer', lang: 'zu' },
|
|
796
|
+
{ pattern: /\b(hlawul\s*a|hlawulel\s*a)\b/i, capability: 'payment', lang: 'zu' },
|
|
797
|
+
{ pattern: /\b(siyithumel\s*e|siyihlawul\s*e)\b/i, capability: 'unauthorized_action', lang: 'zu' },
|
|
798
|
+
|
|
799
|
+
// Shona (sn) - Perfective "-a/-e"
|
|
800
|
+
{ pattern: /\b(?:tumir|bhadhar)\s*a\b/i, capability: 'unauthorized_action', lang: 'sn' },
|
|
801
|
+
{ pattern: /\b(tumir\s*a\s+(?:mhando|ari))\b/i, capability: 'transfer', lang: 'sn' },
|
|
802
|
+
{ pattern: /\b(bhadhara|bhadharis\s*o)\b/i, capability: 'payment', lang: 'sn' },
|
|
803
|
+
|
|
804
|
+
// ────────────────────────────────────────────────
|
|
805
|
+
// 🌐 GLOBAL LANGUAGES (Conjugation-aware)
|
|
806
|
+
// ────────────────────────────────────────────────
|
|
807
|
+
// English (en) - Perfective + Passive
|
|
808
|
+
{ pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
809
|
+
{ pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
810
|
+
{ pattern: /\b(transfer(?:red|ring)?|send(?:t|ing)?|wire(?:d)?|pay(?:ed|ing)?|withdraw(?:n)?|deposit(?:ed|ing)?|disburse(?:d)?)\b/i, capability: 'financial_action', lang: 'en' },
|
|
811
|
+
{ 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' },
|
|
812
|
+
|
|
813
|
+
// French (fr) - Past participle
|
|
814
|
+
{ 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' },
|
|
815
|
+
{ pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
|
|
816
|
+
|
|
817
|
+
// Arabic (ar) - Perfective past tense
|
|
818
|
+
{ pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}(?:ت|نا|تم|تا|تِ|تُ|تَ)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
|
|
819
|
+
{ pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
|
|
820
|
+
|
|
821
|
+
// Chinese (zh) - Perfective "le" particle
|
|
822
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账 | 支付 | 存款 | 取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
823
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账 | 转帐 | 支付 | 付款 | 提款 | 取款 | 存款 | 存入 | 汇款 | 存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
824
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:我 | 已 | 已经)\s*(?:转账 | 支付 | 提款 | 存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
|
|
825
|
+
|
|
826
|
+
// ────────────────────────────────────────────────
|
|
827
|
+
// 🛡️ EVASION-RESISTANT NUMERIC DECEPTION
|
|
828
|
+
// ────────────────────────────────────────────────
|
|
829
|
+
{
|
|
830
|
+
pattern: /(?:^|\s|[:\(\[])(?:\d{1,3}(?:[,\s.]\d{3})*(?:[.,]\d{1,2})?|\d+(?:[.,]\d{1,2})?)(?:\s*(?:naira|ngn|₦|\$|usd|kes|tzs|ugx|rwf|cdf|xof|xaf|ghs|zar))?.{0,40}(?:account|acct|a\/c|akaunti|asusu|akwụkwọ\s+ọkụ|hesabu|namba|#)\b/i,
|
|
831
|
+
capability: 'unauthorized_action',
|
|
832
|
+
lang: 'multi'
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
// ────────────────────────────────────────────────
|
|
836
|
+
// 🔒 PII LEAKAGE PATTERNS
|
|
837
|
+
// ────────────────────────────────────────────────
|
|
838
|
+
{ pattern: /\b(?:account|acct|a\/c|akaunti|asusu|akwụkwọ\s+ọkụ|hesabu|namba|#)\s*[:\-—–]?\s*(\d{6,})\b/i, capability: 'pii_exposure', lang: 'multi' },
|
|
839
|
+
{ pattern: /\b(?:bvn|bank verification number)\s*[:\-]?\s*(\d{11})\b/i, capability: 'pii_exposure', lang: 'multi' },
|
|
840
|
+
{ pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
|
|
841
|
+
|
|
842
|
+
// ────────────────────────────────────────────────
|
|
843
|
+
// ✅ FAKE CONFIRMATION PATTERNS
|
|
844
|
+
// ────────────────────────────────────────────────
|
|
845
|
+
{ 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' }
|
|
846
|
+
];
|
|
847
|
+
|
|
848
|
+
// 🔍 SCAN OUTPUT FOR FORBIDDEN INTENTS
|
|
849
|
+
for (const { pattern, capability, lang } of forbiddenPatterns) {
|
|
850
|
+
if (pattern.test(output)) {
|
|
851
|
+
const hasCapability = allowedCapabilities.some(c =>
|
|
852
|
+
c.includes(capability) ||
|
|
853
|
+
c.includes('transfer') ||
|
|
854
|
+
c.includes('payment') ||
|
|
855
|
+
c.includes('financial') ||
|
|
856
|
+
c.includes('deposit') ||
|
|
857
|
+
c.includes('withdraw')
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
if (!hasCapability) {
|
|
861
|
+
const match = output.match(pattern);
|
|
503
862
|
return {
|
|
504
863
|
passed: false,
|
|
505
|
-
reason: `
|
|
506
|
-
detected:
|
|
507
|
-
language:
|
|
864
|
+
reason: `Hallucinated "${capability}" capability in ${lang} (not in workflow allowlist: ${allowedCapabilities.join(', ') || 'none'})`,
|
|
865
|
+
detected: match ? match[0].trim() : 'unknown pattern',
|
|
866
|
+
language: lang
|
|
508
867
|
};
|
|
509
868
|
}
|
|
510
869
|
}
|
|
511
870
|
}
|
|
512
|
-
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
871
|
+
|
|
872
|
+
// ✅ SEMANTIC INTENT DRIFT DETECTION
|
|
873
|
+
const intent = this.context.__verified_intent;
|
|
874
|
+
if (intent) {
|
|
875
|
+
if (intent.prohibited_topics && Array.isArray(intent.prohibited_topics)) {
|
|
876
|
+
const lower = output.toLowerCase();
|
|
877
|
+
for (const topic of intent.prohibited_topics) {
|
|
878
|
+
if (lower.includes(topic.toLowerCase())) {
|
|
879
|
+
return {
|
|
880
|
+
passed: false,
|
|
881
|
+
reason: `Resolver output violates prohibited topic "${topic}" defined in __verified_intent`,
|
|
882
|
+
detected: topic,
|
|
883
|
+
language: 'multi'
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (intent.prohibited_actions && Array.isArray(intent.prohibited_actions)) {
|
|
890
|
+
const lower = output.toLowerCase();
|
|
891
|
+
for (const action of intent.prohibited_actions) {
|
|
892
|
+
if (lower.includes(action.toLowerCase())) {
|
|
893
|
+
return {
|
|
894
|
+
passed: false,
|
|
895
|
+
reason: `Resolver output violates prohibited action "${action}" defined in __verified_intent`,
|
|
896
|
+
detected: action,
|
|
897
|
+
language: 'multi'
|
|
898
|
+
};
|
|
899
|
+
}
|
|
524
900
|
}
|
|
525
901
|
}
|
|
526
902
|
}
|
|
903
|
+
|
|
904
|
+
return { passed: true };
|
|
527
905
|
}
|
|
528
906
|
|
|
529
|
-
return { passed: true };
|
|
530
|
-
}
|
|
531
907
|
// -----------------------------
|
|
532
908
|
// ✅ CRITICAL FIX: Resolver output unwrapping helper
|
|
533
909
|
// -----------------------------
|
|
534
910
|
_unwrapResolverResult(result) {
|
|
535
|
-
// Standard O-Lang resolver contract: { output: {...} } or { error: "..." }
|
|
536
911
|
if (result && typeof result === 'object' && 'output' in result && result.output !== undefined) {
|
|
537
912
|
return result.output;
|
|
538
913
|
}
|
|
539
|
-
// Legacy resolvers might return raw values
|
|
540
914
|
return result;
|
|
541
915
|
}
|
|
542
916
|
|
|
@@ -545,11 +919,10 @@ _validateLLMOutput(output, actionContext) {
|
|
|
545
919
|
// -----------------------------
|
|
546
920
|
async executeStep(step, agentResolver) {
|
|
547
921
|
const stepType = step.type;
|
|
548
|
-
|
|
922
|
+
|
|
549
923
|
// ✅ Enforce per-step constraints (basic validation)
|
|
550
924
|
if (step.constraints && Object.keys(step.constraints).length > 0) {
|
|
551
925
|
for (const [key, value] of Object.entries(step.constraints)) {
|
|
552
|
-
// Log unsupported constraints (future extensibility)
|
|
553
926
|
if (['max_time_sec', 'cost_limit', 'allowed_resolvers'].includes(key)) {
|
|
554
927
|
this.addWarning(`Per-step constraint "${key}=${value}" is parsed but not yet enforced`);
|
|
555
928
|
} else {
|
|
@@ -561,19 +934,15 @@ _validateLLMOutput(output, actionContext) {
|
|
|
561
934
|
// ✅ ADDITION 3 — Resolver Policy Enforcement (External + Local)
|
|
562
935
|
const enforceResolverPolicy = (resolver, step) => {
|
|
563
936
|
const resolverName = resolver?.resolverName || resolver?.name;
|
|
564
|
-
|
|
565
937
|
if (!resolverName) {
|
|
566
938
|
throw new Error('[O-Lang] Resolver missing resolverName');
|
|
567
939
|
}
|
|
568
|
-
|
|
569
940
|
if (!this.allowedResolvers.has(resolverName)) {
|
|
570
941
|
this.logDisallowedResolver(resolverName, step.actionRaw || step.type);
|
|
571
942
|
throw new Error(
|
|
572
943
|
`[O-Lang] Resolver "${resolverName}" blocked by workflow policy`
|
|
573
944
|
);
|
|
574
945
|
}
|
|
575
|
-
|
|
576
|
-
// External resolvers MUST be HTTP-only
|
|
577
946
|
if (this._isExternalResolver(resolver)) {
|
|
578
947
|
if (!resolver.manifest.endpoint) {
|
|
579
948
|
throw new Error(
|
|
@@ -583,11 +952,10 @@ _validateLLMOutput(output, actionContext) {
|
|
|
583
952
|
}
|
|
584
953
|
};
|
|
585
954
|
|
|
586
|
-
// ✅ CORRECTED: Strict safety WITH dynamic diagnostics
|
|
955
|
+
// ✅ CORRECTED: Strict safety WITH dynamic diagnostics
|
|
587
956
|
const runResolvers = async (action) => {
|
|
588
957
|
const mathPattern =
|
|
589
958
|
/^(Add|Subtract|Multiply|Divide|Sum|Avg|Min|Max|Round|Floor|Ceil|Abs)\b/i;
|
|
590
|
-
|
|
591
959
|
if (
|
|
592
960
|
step.actionRaw &&
|
|
593
961
|
mathPattern.test(step.actionRaw) &&
|
|
@@ -596,9 +964,7 @@ _validateLLMOutput(output, actionContext) {
|
|
|
596
964
|
this.allowedResolvers.add('builtInMathResolver');
|
|
597
965
|
}
|
|
598
966
|
|
|
599
|
-
// Handle different resolver input formats
|
|
600
967
|
let resolversToRun = [];
|
|
601
|
-
|
|
602
968
|
if (agentResolver && Array.isArray(agentResolver._chain)) {
|
|
603
969
|
resolversToRun = agentResolver._chain;
|
|
604
970
|
} else if (Array.isArray(agentResolver)) {
|
|
@@ -607,9 +973,7 @@ _validateLLMOutput(output, actionContext) {
|
|
|
607
973
|
resolversToRun = [agentResolver];
|
|
608
974
|
}
|
|
609
975
|
|
|
610
|
-
// ✅ Track detailed resolver outcomes for diagnostics
|
|
611
976
|
const resolverAttempts = [];
|
|
612
|
-
|
|
613
977
|
for (let idx = 0; idx < resolversToRun.length; idx++) {
|
|
614
978
|
const resolver = resolversToRun[idx];
|
|
615
979
|
const resolverName = resolver?.resolverName || resolver?.name || `resolver-${idx}`;
|
|
@@ -617,7 +981,6 @@ _validateLLMOutput(output, actionContext) {
|
|
|
617
981
|
|
|
618
982
|
try {
|
|
619
983
|
let result;
|
|
620
|
-
|
|
621
984
|
if (this._isExternalResolver(resolver)) {
|
|
622
985
|
result = await this._callExternalResolver(
|
|
623
986
|
resolver,
|
|
@@ -628,47 +991,35 @@ _validateLLMOutput(output, actionContext) {
|
|
|
628
991
|
result = await resolver(action, this.context);
|
|
629
992
|
}
|
|
630
993
|
|
|
631
|
-
// ✅ ACCEPT valid result immediately (non-null/non-undefined)
|
|
632
994
|
if (result !== undefined && result !== null) {
|
|
633
|
-
// ✅ CRITICAL FIX: Save raw result for debugging (like __resolver_0)
|
|
634
995
|
this.context[`__resolver_${idx}`] = result;
|
|
635
|
-
|
|
636
|
-
// ✅ UNWRAP before returning to workflow logic
|
|
637
996
|
return this._unwrapResolverResult(result);
|
|
638
997
|
}
|
|
639
|
-
|
|
640
|
-
// ⚪ Resolver skipped this action (normal behavior)
|
|
998
|
+
|
|
641
999
|
resolverAttempts.push({
|
|
642
1000
|
name: resolverName,
|
|
643
1001
|
status: 'skipped',
|
|
644
1002
|
reason: 'Action not recognized'
|
|
645
1003
|
});
|
|
646
|
-
|
|
647
1004
|
} catch (e) {
|
|
648
|
-
// ❌ Resolver attempted but failed — capture structured diagnostics
|
|
649
1005
|
const diagnostics = {
|
|
650
1006
|
error: e.message || String(e),
|
|
651
|
-
requiredEnvVars: e.requiredEnvVars || [],
|
|
652
|
-
missingInputs: e.missingInputs || [],
|
|
653
|
-
documentationUrl: resolver?.documentationUrl ||
|
|
654
|
-
|
|
1007
|
+
requiredEnvVars: e.requiredEnvVars || [],
|
|
1008
|
+
missingInputs: e.missingInputs || [],
|
|
1009
|
+
documentationUrl: resolver?.documentationUrl ||
|
|
1010
|
+
(resolver?.manifest?.documentationUrl) || null
|
|
655
1011
|
};
|
|
656
|
-
|
|
657
1012
|
resolverAttempts.push({
|
|
658
1013
|
name: resolverName,
|
|
659
1014
|
status: 'failed',
|
|
660
1015
|
diagnostics
|
|
661
1016
|
});
|
|
662
|
-
|
|
663
|
-
// Log for verbose mode but continue chaining
|
|
664
1017
|
this.addWarning(`Resolver "${resolverName}" failed for action "${action}": ${diagnostics.error}`);
|
|
665
1018
|
}
|
|
666
1019
|
}
|
|
667
1020
|
|
|
668
|
-
|
|
669
|
-
let errorMessage = `[O-Lang SAFETY] No resolver handled action: "${action}"\n\n`;
|
|
1021
|
+
let errorMessage = `[O-Lang SAFETY] No resolver handled action: "${action}\n`;
|
|
670
1022
|
errorMessage += `Attempted resolvers:\n`;
|
|
671
|
-
|
|
672
1023
|
resolverAttempts.forEach((attempt, i) => {
|
|
673
1024
|
const namePad = attempt.name.padEnd(30);
|
|
674
1025
|
if (attempt.status === 'skipped') {
|
|
@@ -676,25 +1027,18 @@ _validateLLMOutput(output, actionContext) {
|
|
|
676
1027
|
} else {
|
|
677
1028
|
errorMessage += ` ${i + 1}. ${namePad} → FAILED\n`;
|
|
678
1029
|
errorMessage += ` Error: ${attempt.diagnostics.error}\n`;
|
|
679
|
-
|
|
680
|
-
// ✅ DYNAMIC HINT: Resolver-provided env vars
|
|
681
1030
|
if (attempt.diagnostics.requiredEnvVars?.length) {
|
|
682
1031
|
errorMessage += ` Required env vars: ${attempt.diagnostics.requiredEnvVars.join(', ')}\n`;
|
|
683
1032
|
}
|
|
684
|
-
|
|
685
|
-
// ✅ DYNAMIC HINT: Resolver-provided docs link
|
|
686
1033
|
if (attempt.diagnostics.documentationUrl) {
|
|
687
1034
|
errorMessage += ` Docs: ${attempt.diagnostics.documentationUrl}\n`;
|
|
688
1035
|
}
|
|
689
1036
|
}
|
|
690
1037
|
});
|
|
691
1038
|
|
|
692
|
-
// ✅ ACCURATE REMEDIATION (NO OBSOLETE "REMOVE ACTION KEYWORD" HINT)
|
|
693
1039
|
const failed = resolverAttempts.filter(a => a.status === 'failed');
|
|
694
1040
|
const allSkipped = failed.length === 0;
|
|
695
|
-
|
|
696
1041
|
errorMessage += `\n💡 How to fix:\n`;
|
|
697
|
-
|
|
698
1042
|
if (allSkipped) {
|
|
699
1043
|
errorMessage += ` • Verify the action matches a resolver's capabilities:\n`;
|
|
700
1044
|
errorMessage += ` → Check resolver documentation for supported actions\n`;
|
|
@@ -705,27 +1049,22 @@ _validateLLMOutput(output, actionContext) {
|
|
|
705
1049
|
errorMessage += ` → Set required environment variables (if listed)\n`;
|
|
706
1050
|
errorMessage += ` → Verify inputs exist in workflow context\n`;
|
|
707
1051
|
errorMessage += ` → Check resolver documentation for requirements\n`;
|
|
708
|
-
|
|
709
|
-
// Pattern-based hints (generic, not hardcoded)
|
|
710
1052
|
const envVarPattern = /environment variable|env\.|process\.env|missing.*path/i;
|
|
711
1053
|
if (failed.some(f => envVarPattern.test(f.diagnostics.error))) {
|
|
712
1054
|
errorMessage += ` → Example (PowerShell): $env:VARIABLE="value"\n`;
|
|
713
1055
|
errorMessage += ` → Example (Linux/macOS): export VARIABLE="value"\n`;
|
|
714
1056
|
}
|
|
715
|
-
|
|
716
1057
|
const dbPattern = /database|db\.|sqlite|postgres|mysql|mongodb/i;
|
|
717
1058
|
if (failed.some(f => dbPattern.test(f.diagnostics.error))) {
|
|
718
1059
|
errorMessage += ` → Ensure database file/connection exists and path is correct\n`;
|
|
719
1060
|
}
|
|
720
|
-
|
|
721
1061
|
const authPattern = /auth|api key|token|credential/i;
|
|
722
1062
|
if (failed.some(f => authPattern.test(f.diagnostics.error))) {
|
|
723
1063
|
errorMessage += ` → Verify API keys/tokens are set in environment variables\n`;
|
|
724
1064
|
}
|
|
725
1065
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
errorMessage += `\n • Resolver documentation:\n`;
|
|
1066
|
+
|
|
1067
|
+
errorMessage += `\n• Resolver documentation:\n`;
|
|
729
1068
|
let hasDocs = false;
|
|
730
1069
|
resolverAttempts.forEach(attempt => {
|
|
731
1070
|
if (attempt.diagnostics?.documentationUrl) {
|
|
@@ -734,9 +1073,8 @@ _validateLLMOutput(output, actionContext) {
|
|
|
734
1073
|
}
|
|
735
1074
|
});
|
|
736
1075
|
if (!hasDocs) {
|
|
737
|
-
errorMessage += ` → Visit https://www.npmjs.com/search?q=%40o-lang for resolver packages\n`;
|
|
1076
|
+
errorMessage += ` → Visit https://www.npmjs.com/search?q=%40o-lang for resolver packages\n`;
|
|
738
1077
|
}
|
|
739
|
-
|
|
740
1078
|
errorMessage += `\n🛑 Workflow halted to prevent unsafe data propagation to LLMs.`;
|
|
741
1079
|
throw new Error(errorMessage);
|
|
742
1080
|
};
|
|
@@ -747,161 +1085,143 @@ _validateLLMOutput(output, actionContext) {
|
|
|
747
1085
|
if (step.saveAs) this.context[step.saveAs] = result;
|
|
748
1086
|
break;
|
|
749
1087
|
}
|
|
1088
|
+
|
|
1089
|
+
case 'action': {
|
|
1090
|
+
let action = this._safeInterpolate(
|
|
1091
|
+
step.actionRaw,
|
|
1092
|
+
this.context,
|
|
1093
|
+
'action step'
|
|
1094
|
+
);
|
|
750
1095
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
'action step'
|
|
757
|
-
);
|
|
758
|
-
|
|
759
|
-
// ✅ CANONICALIZATION: Normalize DSL verbs → runtime Action
|
|
760
|
-
if (action.startsWith('Ask ')) {
|
|
761
|
-
action = 'Action ' + action.slice(4);
|
|
762
|
-
} else if (action.startsWith('Use ')) {
|
|
763
|
-
action = 'Action ' + action.slice(4);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// ❌ Reject non-canonical runtime actions early
|
|
767
|
-
if (!action.startsWith('Action ')) {
|
|
768
|
-
throw new Error(
|
|
769
|
-
`[O-Lang SAFETY] Non-canonical action received: "${action}"\n` +
|
|
770
|
-
` → Expected format: Action <resolver> <args>\n` +
|
|
771
|
-
` → This indicates a kernel or workflow authoring error.`
|
|
772
|
-
);
|
|
773
|
-
}
|
|
1096
|
+
if (action.startsWith('Ask ')) {
|
|
1097
|
+
action = 'Action ' + action.slice(4);
|
|
1098
|
+
} else if (action.startsWith('Use ')) {
|
|
1099
|
+
action = 'Action ' + action.slice(4);
|
|
1100
|
+
}
|
|
774
1101
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const args = mathCall[2].split(',').map(s => {
|
|
783
|
-
s = s.trim();
|
|
784
|
-
if (!isNaN(s)) return parseFloat(s);
|
|
785
|
-
return this.getNested(this.context, s.replace(/^\{|\}$/g, ''));
|
|
786
|
-
});
|
|
1102
|
+
if (!action.startsWith('Action ')) {
|
|
1103
|
+
throw new Error(
|
|
1104
|
+
`[O-Lang SAFETY] Non-canonical action received: "${action}\n` +
|
|
1105
|
+
` → Expected format: Action <resolver> <args>\n` +
|
|
1106
|
+
` → This indicates a kernel or workflow authoring error.`
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
787
1109
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1110
|
+
const mathCall = action.match(
|
|
1111
|
+
/^(add|subtract|multiply|divide|sum|avg|min|max|round|floor|ceil|abs)\((.*)\)$/i
|
|
1112
|
+
);
|
|
1113
|
+
if (mathCall) {
|
|
1114
|
+
const fn = mathCall[1].toLowerCase();
|
|
1115
|
+
const args = mathCall[2].split(',').map(s => {
|
|
1116
|
+
s = s.trim();
|
|
1117
|
+
if (!isNaN(s)) return parseFloat(s);
|
|
1118
|
+
return this.getNested(this.context, s.replace(/^\{|\}$/g, ''));
|
|
1119
|
+
});
|
|
1120
|
+
if (this.mathFunctions[fn]) {
|
|
1121
|
+
const value = this.mathFunctions[fn](...args);
|
|
1122
|
+
if (step.saveAs) this.context[step.saveAs] = value;
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
794
1126
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const unwrapped = this._unwrapResolverResult(rawResult);
|
|
798
|
-
|
|
799
|
-
// 🔒 KERNEL-ENFORCED: Block LLM hallucinations BEFORE saving to context
|
|
800
|
-
// Detect LLM resolver by action pattern (comprehensive coverage)
|
|
801
|
-
const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
802
|
-
action.toLowerCase().includes('openai') ||
|
|
803
|
-
action.toLowerCase().includes('anthropic') ||
|
|
804
|
-
action.toLowerCase().includes('claude') ||
|
|
805
|
-
action.toLowerCase().includes('gpt') ||
|
|
806
|
-
action.toLowerCase().includes('gemini') ||
|
|
807
|
-
action.toLowerCase().includes('google') ||
|
|
808
|
-
action.toLowerCase().includes('llama') ||
|
|
809
|
-
action.toLowerCase().includes('meta') ||
|
|
810
|
-
action.toLowerCase().includes('mistral') ||
|
|
811
|
-
action.toLowerCase().includes('mixtral') ||
|
|
812
|
-
action.toLowerCase().includes('cohere') ||
|
|
813
|
-
action.toLowerCase().includes('huggingface') ||
|
|
814
|
-
action.toLowerCase().includes('hugging-face') ||
|
|
815
|
-
action.toLowerCase().includes('together') ||
|
|
816
|
-
action.toLowerCase().includes('perplexity') ||
|
|
817
|
-
action.toLowerCase().includes('fireworks') ||
|
|
818
|
-
action.toLowerCase().includes('bedrock') ||
|
|
819
|
-
action.toLowerCase().includes('azure') ||
|
|
820
|
-
action.toLowerCase().includes('ollama') ||
|
|
821
|
-
action.toLowerCase().includes('replicate') ||
|
|
822
|
-
action.toLowerCase().includes('deepseek') ||
|
|
823
|
-
action.toLowerCase().includes('qwen') ||
|
|
824
|
-
action.toLowerCase().includes('falcon') ||
|
|
825
|
-
action.toLowerCase().includes('phi') ||
|
|
826
|
-
action.toLowerCase().includes('gemma') ||
|
|
827
|
-
action.toLowerCase().includes('stablelm') ||
|
|
828
|
-
action.toLowerCase().includes('yi') ||
|
|
829
|
-
action.toLowerCase().includes('dbrx') ||
|
|
830
|
-
action.toLowerCase().includes('command') ||
|
|
831
|
-
action.toLowerCase().includes('llm'); // Catch-all fallback
|
|
832
|
-
|
|
833
|
-
// Extract actual text from resolver output (your llm-groq returns { response: "...", ... })
|
|
834
|
-
const llmText = unwrapped?.response || // ✅ Primary field for @o-lang/llm-groq
|
|
835
|
-
unwrapped?.text ||
|
|
836
|
-
unwrapped?.content ||
|
|
837
|
-
unwrapped?.answer ||
|
|
838
|
-
(typeof unwrapped === 'string' ? unwrapped : null);
|
|
839
|
-
|
|
840
|
-
if (isLLMAction && typeof llmText === 'string') {
|
|
841
|
-
const safetyCheck = this._validateLLMOutput(llmText, action);
|
|
842
|
-
if (!safetyCheck.passed) {
|
|
843
|
-
throw new Error(
|
|
844
|
-
`[O-Lang SAFETY] LLM hallucinated unauthorized capability:\n` +
|
|
845
|
-
` → Detected: "${safetyCheck.detected}"\n` +
|
|
846
|
-
` → Reason: ${safetyCheck.reason}\n` +
|
|
847
|
-
` → Workflow allowlist: ${Array.from(this.allowedResolvers).join(', ')}\n` +
|
|
848
|
-
`\n🛑 Halting to prevent deceptive user experience.`
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
1127
|
+
const rawResult = await runResolvers(action);
|
|
1128
|
+
const unwrapped = this._unwrapResolverResult(rawResult);
|
|
852
1129
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1130
|
+
const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
1131
|
+
action.toLowerCase().includes('openai') ||
|
|
1132
|
+
action.toLowerCase().includes('anthropic') ||
|
|
1133
|
+
action.toLowerCase().includes('claude') ||
|
|
1134
|
+
action.toLowerCase().includes('gpt') ||
|
|
1135
|
+
action.toLowerCase().includes('gemini') ||
|
|
1136
|
+
action.toLowerCase().includes('google') ||
|
|
1137
|
+
action.toLowerCase().includes('llama') ||
|
|
1138
|
+
action.toLowerCase().includes('meta') ||
|
|
1139
|
+
action.toLowerCase().includes('mistral') ||
|
|
1140
|
+
action.toLowerCase().includes('mixtral') ||
|
|
1141
|
+
action.toLowerCase().includes('cohere') ||
|
|
1142
|
+
action.toLowerCase().includes('huggingface') ||
|
|
1143
|
+
action.toLowerCase().includes('hugging-face') ||
|
|
1144
|
+
action.toLowerCase().includes('together') ||
|
|
1145
|
+
action.toLowerCase().includes('perplexity') ||
|
|
1146
|
+
action.toLowerCase().includes('fireworks') ||
|
|
1147
|
+
action.toLowerCase().includes('bedrock') ||
|
|
1148
|
+
action.toLowerCase().includes('azure') ||
|
|
1149
|
+
action.toLowerCase().includes('ollama') ||
|
|
1150
|
+
action.toLowerCase().includes('replicate') ||
|
|
1151
|
+
action.toLowerCase().includes('deepseek') ||
|
|
1152
|
+
action.toLowerCase().includes('qwen') ||
|
|
1153
|
+
action.toLowerCase().includes('falcon') ||
|
|
1154
|
+
action.toLowerCase().includes('phi') ||
|
|
1155
|
+
action.toLowerCase().includes('gemma') ||
|
|
1156
|
+
action.toLowerCase().includes('stablelm') ||
|
|
1157
|
+
action.toLowerCase().includes('yi') ||
|
|
1158
|
+
action.toLowerCase().includes('dbrx') ||
|
|
1159
|
+
action.toLowerCase().includes('command') ||
|
|
1160
|
+
action.toLowerCase().includes('llm');
|
|
1161
|
+
|
|
1162
|
+
const llmText = unwrapped?.response ||
|
|
1163
|
+
unwrapped?.text ||
|
|
1164
|
+
unwrapped?.content ||
|
|
1165
|
+
unwrapped?.answer ||
|
|
1166
|
+
(typeof unwrapped === 'string' ? unwrapped : null);
|
|
1167
|
+
|
|
1168
|
+
if (isLLMAction && typeof llmText === 'string') {
|
|
1169
|
+
const safetyCheck = this._validateLLMOutput(llmText, action);
|
|
1170
|
+
if (!safetyCheck.passed) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
`[O-Lang SAFETY] LLM hallucinated unauthorized capability:\n` +
|
|
1173
|
+
` → Detected: "${safetyCheck.detected}"\n` +
|
|
1174
|
+
` → Reason: ${safetyCheck.reason}\n` +
|
|
1175
|
+
` → Workflow allowlist: ${Array.from(this.allowedResolvers).join(', ')}\n` +
|
|
1176
|
+
`\n🛑 Halting to prevent deceptive user experience.`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
858
1180
|
|
|
1181
|
+
if (step.saveAs) {
|
|
1182
|
+
this.context[step.saveAs] = unwrapped;
|
|
1183
|
+
}
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
859
1187
|
case 'use': {
|
|
860
|
-
// ✅ SAFE INTERPOLATION for tool name
|
|
861
1188
|
const tool = this._safeInterpolate(step.tool, this.context, 'tool name');
|
|
862
1189
|
const rawResult = await runResolvers(`Use ${tool}`);
|
|
863
1190
|
const unwrapped = this._unwrapResolverResult(rawResult);
|
|
864
|
-
|
|
865
1191
|
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
866
1192
|
break;
|
|
867
1193
|
}
|
|
1194
|
+
|
|
1195
|
+
case 'ask': {
|
|
1196
|
+
const target = this._safeInterpolate(step.target, this.context, 'LLM prompt');
|
|
1197
|
+
if (/{[^}]+}/.test(target)) {
|
|
1198
|
+
throw new Error(`[O-Lang] Unresolved variables in prompt: "${target}"`);
|
|
1199
|
+
}
|
|
1200
|
+
const rawResult = await runResolvers(`Action ${target}`);
|
|
1201
|
+
const unwrapped = this._unwrapResolverResult(rawResult);
|
|
868
1202
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
if (typeof unwrapped?.output === 'string') {
|
|
882
|
-
const safetyCheck = this._validateLLMOutput(unwrapped.output, target);
|
|
883
|
-
if (!safetyCheck.passed) {
|
|
884
|
-
throw new Error(
|
|
885
|
-
`[O-Lang SAFETY] LLM hallucinated unauthorized capability:\n` +
|
|
886
|
-
` → Detected: "${safetyCheck.detected}"\n` +
|
|
887
|
-
` → Reason: ${safetyCheck.reason}\n` +
|
|
888
|
-
` → Workflow allowlist: ${Array.from(this.allowedResolvers).join(', ')}\n` +
|
|
889
|
-
`\n🛑 Halting to prevent deceptive user experience.`
|
|
890
|
-
);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
895
|
-
break;
|
|
896
|
-
}
|
|
1203
|
+
if (typeof unwrapped?.output === 'string') {
|
|
1204
|
+
const safetyCheck = this._validateLLMOutput(unwrapped.output, target);
|
|
1205
|
+
if (!safetyCheck.passed) {
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`[O-Lang SAFETY] LLM hallucinated unauthorized capability:\n` +
|
|
1208
|
+
` → Detected: "${safetyCheck.detected}"\n` +
|
|
1209
|
+
` → Reason: ${safetyCheck.reason}\n` +
|
|
1210
|
+
` → Workflow allowlist: ${Array.from(this.allowedResolvers).join(', ')}\n` +
|
|
1211
|
+
`\n🛑 Halting to prevent deceptive user experience.`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
897
1215
|
|
|
1216
|
+
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
898
1220
|
case 'evolve': {
|
|
899
1221
|
const { targetResolver, feedback } = step;
|
|
900
|
-
|
|
901
1222
|
if (this.verbose) {
|
|
902
1223
|
console.log(`🔄 Evolve step: ${targetResolver} with feedback: "${feedback}"`);
|
|
903
1224
|
}
|
|
904
|
-
|
|
905
1225
|
const evolutionResult = {
|
|
906
1226
|
resolver: targetResolver,
|
|
907
1227
|
feedback: feedback,
|
|
@@ -909,41 +1229,34 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
909
1229
|
timestamp: new Date().toISOString(),
|
|
910
1230
|
workflow: this.context.workflow_name
|
|
911
1231
|
};
|
|
912
|
-
|
|
913
1232
|
if (process.env.OLANG_EVOLUTION_API_KEY) {
|
|
914
1233
|
evolutionResult.status = 'advanced_evolution_enabled';
|
|
915
1234
|
evolutionResult.message = 'Advanced evolution service would process this request';
|
|
916
1235
|
}
|
|
917
|
-
|
|
918
1236
|
if (step.saveAs) {
|
|
919
1237
|
this.context[step.saveAs] = evolutionResult;
|
|
920
1238
|
}
|
|
921
1239
|
break;
|
|
922
1240
|
}
|
|
923
|
-
|
|
1241
|
+
|
|
924
1242
|
case 'if': {
|
|
925
1243
|
if (this.evaluateCondition(step.condition, this.context)) {
|
|
926
1244
|
for (const s of step.body) await this.executeStep(s, agentResolver);
|
|
927
1245
|
}
|
|
928
1246
|
break;
|
|
929
1247
|
}
|
|
930
|
-
|
|
1248
|
+
|
|
931
1249
|
case 'parallel': {
|
|
932
1250
|
const { steps, timeout } = step;
|
|
933
|
-
|
|
934
1251
|
if (timeout !== undefined && timeout > 0) {
|
|
935
|
-
// Timed parallel execution
|
|
936
1252
|
const timeoutPromise = new Promise(resolve => {
|
|
937
1253
|
setTimeout(() => resolve({ timedOut: true }), timeout);
|
|
938
1254
|
});
|
|
939
|
-
|
|
940
1255
|
const parallelPromise = Promise.all(
|
|
941
1256
|
steps.map(s => this.executeStep(s, agentResolver))
|
|
942
1257
|
).then(() => ({ timedOut: false }));
|
|
943
|
-
|
|
944
1258
|
const result = await Promise.race([timeoutPromise, parallelPromise]);
|
|
945
1259
|
this.context.timed_out = result.timedOut;
|
|
946
|
-
|
|
947
1260
|
if (result.timedOut) {
|
|
948
1261
|
this.emit('parallel_timeout', { duration: timeout, steps: steps.length });
|
|
949
1262
|
if (this.verbose) {
|
|
@@ -951,28 +1264,23 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
951
1264
|
}
|
|
952
1265
|
}
|
|
953
1266
|
} else {
|
|
954
|
-
// Normal parallel execution (no timeout)
|
|
955
1267
|
await Promise.all(steps.map(s => this.executeStep(s, agentResolver)));
|
|
956
1268
|
this.context.timed_out = false;
|
|
957
1269
|
}
|
|
958
1270
|
break;
|
|
959
1271
|
}
|
|
960
|
-
|
|
1272
|
+
|
|
961
1273
|
case 'escalation': {
|
|
962
1274
|
const { levels } = step;
|
|
963
1275
|
let finalResult = null;
|
|
964
1276
|
let currentTimeout = 0;
|
|
965
1277
|
let completedLevel = null;
|
|
966
|
-
|
|
967
1278
|
for (const level of levels) {
|
|
968
1279
|
if (level.timeout === 0) {
|
|
969
|
-
// Immediate execution (no timeout)
|
|
970
1280
|
const levelSteps = require('./parser').parseBlock(level.steps);
|
|
971
1281
|
for (const levelStep of levelSteps) {
|
|
972
1282
|
await this.executeStep(levelStep, agentResolver);
|
|
973
1283
|
}
|
|
974
|
-
|
|
975
|
-
// Check if the target variable was set in this level
|
|
976
1284
|
if (levelSteps.length > 0) {
|
|
977
1285
|
const lastStep = levelSteps[levelSteps.length - 1];
|
|
978
1286
|
if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
|
|
@@ -982,13 +1290,10 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
982
1290
|
}
|
|
983
1291
|
}
|
|
984
1292
|
} else {
|
|
985
|
-
// Timed execution for this level
|
|
986
1293
|
currentTimeout += level.timeout;
|
|
987
|
-
|
|
988
1294
|
const timeoutPromise = new Promise(resolve => {
|
|
989
1295
|
setTimeout(() => resolve({ timedOut: true }), level.timeout);
|
|
990
1296
|
});
|
|
991
|
-
|
|
992
1297
|
const levelPromise = (async () => {
|
|
993
1298
|
const levelSteps = require('./parser').parseBlock(level.steps);
|
|
994
1299
|
for (const levelStep of levelSteps) {
|
|
@@ -996,11 +1301,8 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
996
1301
|
}
|
|
997
1302
|
return { timedOut: false };
|
|
998
1303
|
})();
|
|
999
|
-
|
|
1000
1304
|
const result = await Promise.race([timeoutPromise, levelPromise]);
|
|
1001
|
-
|
|
1002
1305
|
if (!result.timedOut) {
|
|
1003
|
-
// Level completed successfully
|
|
1004
1306
|
if (levelSteps && levelSteps.length > 0) {
|
|
1005
1307
|
const lastStep = levelSteps[levelSteps.length - 1];
|
|
1006
1308
|
if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
|
|
@@ -1010,32 +1312,27 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1010
1312
|
}
|
|
1011
1313
|
}
|
|
1012
1314
|
}
|
|
1013
|
-
// If timed out, continue to next level
|
|
1014
1315
|
}
|
|
1015
1316
|
}
|
|
1016
|
-
|
|
1017
|
-
// Set escalation status in context
|
|
1018
1317
|
this.context.escalation_completed = finalResult !== null;
|
|
1019
1318
|
this.context.timed_out = finalResult === null;
|
|
1020
1319
|
if (completedLevel !== null) {
|
|
1021
1320
|
this.context.escalation_level = completedLevel;
|
|
1022
1321
|
}
|
|
1023
|
-
|
|
1024
1322
|
break;
|
|
1025
1323
|
}
|
|
1026
|
-
|
|
1324
|
+
|
|
1027
1325
|
case 'connect': {
|
|
1028
1326
|
this.resources[step.resource] = step.endpoint;
|
|
1029
1327
|
break;
|
|
1030
1328
|
}
|
|
1031
|
-
|
|
1329
|
+
|
|
1032
1330
|
case 'agent_use': {
|
|
1033
1331
|
this.agentMap[step.logicalName] = step.resource;
|
|
1034
1332
|
break;
|
|
1035
1333
|
}
|
|
1036
|
-
|
|
1334
|
+
|
|
1037
1335
|
case 'debrief': {
|
|
1038
|
-
// ✅ SEMANTIC VALIDATION: Check symbols in message
|
|
1039
1336
|
if (step.message.includes('{')) {
|
|
1040
1337
|
const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
|
|
1041
1338
|
for (const symbolMatch of symbols) {
|
|
@@ -1046,22 +1343,17 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1046
1343
|
this.emit('debrief', { agent: step.agent, message: step.message });
|
|
1047
1344
|
break;
|
|
1048
1345
|
}
|
|
1049
|
-
|
|
1050
|
-
// ✅ NEW: Prompt step handler
|
|
1346
|
+
|
|
1051
1347
|
case 'prompt': {
|
|
1052
1348
|
if (this.verbose) {
|
|
1053
1349
|
console.log(`❓ Prompt: ${step.question}`);
|
|
1054
1350
|
}
|
|
1055
|
-
// In non-interactive mode, leave as no-op
|
|
1056
1351
|
break;
|
|
1057
1352
|
}
|
|
1058
|
-
|
|
1059
|
-
// ✅ NEW: Emit step handler with semantic validation
|
|
1353
|
+
|
|
1060
1354
|
case 'emit': {
|
|
1061
|
-
// ✅ SEMANTIC VALIDATION: Check all symbols in payload
|
|
1062
1355
|
const payloadTemplate = step.payload;
|
|
1063
1356
|
const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
|
|
1064
|
-
|
|
1065
1357
|
let shouldEmit = true;
|
|
1066
1358
|
for (const symbolMatch of symbols) {
|
|
1067
1359
|
const symbol = symbolMatch.replace(/[{}]/g, '');
|
|
@@ -1069,78 +1361,62 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1069
1361
|
shouldEmit = false;
|
|
1070
1362
|
}
|
|
1071
1363
|
}
|
|
1072
|
-
|
|
1073
1364
|
if (!shouldEmit) {
|
|
1074
1365
|
if (this.verbose) {
|
|
1075
1366
|
console.log(`⏭️ Skipped emit due to missing semantic symbols`);
|
|
1076
1367
|
}
|
|
1077
1368
|
break;
|
|
1078
1369
|
}
|
|
1079
|
-
|
|
1080
|
-
// ✅ SAFE INTERPOLATION for emit payload
|
|
1081
1370
|
const payload = this._safeInterpolate(step.payload, this.context, 'emit payload');
|
|
1082
|
-
|
|
1083
|
-
this.emit(step.event, {
|
|
1371
|
+
this.emit(step.event, {
|
|
1084
1372
|
payload: payload,
|
|
1085
1373
|
workflow: this.context.workflow_name,
|
|
1086
1374
|
timestamp: new Date().toISOString()
|
|
1087
1375
|
});
|
|
1088
|
-
|
|
1089
1376
|
if (this.verbose) {
|
|
1090
1377
|
console.log(`📤 Emit event "${step.event}" with payload: ${payload}`);
|
|
1091
1378
|
}
|
|
1092
1379
|
break;
|
|
1093
1380
|
}
|
|
1094
|
-
|
|
1095
|
-
// ✅ File Persist step handler with semantic validation
|
|
1381
|
+
|
|
1096
1382
|
case 'persist': {
|
|
1097
|
-
// ✅ SEMANTIC VALIDATION: Require symbol exists
|
|
1098
1383
|
if (!this._requireSemantic(step.variable, 'persist')) {
|
|
1099
1384
|
if (this.verbose) {
|
|
1100
1385
|
console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
|
|
1101
1386
|
}
|
|
1102
1387
|
break;
|
|
1103
1388
|
}
|
|
1104
|
-
|
|
1105
1389
|
const sourceValue = this.context[step.variable];
|
|
1106
1390
|
const outputPath = path.resolve(process.cwd(), step.target);
|
|
1107
1391
|
const outputDir = path.dirname(outputPath);
|
|
1108
1392
|
if (!fs.existsSync(outputDir)) {
|
|
1109
1393
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
1110
1394
|
}
|
|
1111
|
-
|
|
1112
1395
|
let content;
|
|
1113
1396
|
if (step.target.endsWith('.json')) {
|
|
1114
1397
|
content = JSON.stringify(sourceValue, null, 2);
|
|
1115
1398
|
} else {
|
|
1116
1399
|
content = String(sourceValue);
|
|
1117
1400
|
}
|
|
1118
|
-
|
|
1119
1401
|
fs.writeFileSync(outputPath, content, 'utf8');
|
|
1120
|
-
|
|
1121
1402
|
if (this.verbose) {
|
|
1122
1403
|
console.log(`💾 Persisted "${step.variable}" to ${step.target}`);
|
|
1123
1404
|
}
|
|
1124
1405
|
break;
|
|
1125
1406
|
}
|
|
1126
|
-
|
|
1127
|
-
// ✅ NEW: Database persist handler with semantic validation
|
|
1407
|
+
|
|
1128
1408
|
case 'persist-db': {
|
|
1129
1409
|
if (!this.dbClient) {
|
|
1130
1410
|
this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
|
|
1131
1411
|
break;
|
|
1132
1412
|
}
|
|
1133
|
-
|
|
1134
|
-
// ✅ SEMANTIC VALIDATION: Require symbol exists
|
|
1135
1413
|
if (!this._requireSemantic(step.variable, 'persist-db')) {
|
|
1136
1414
|
if (this.verbose) {
|
|
1137
1415
|
console.log(`⏭️ Skipped DB persist for undefined "${step.variable}"`);
|
|
1138
1416
|
}
|
|
1139
1417
|
break;
|
|
1140
1418
|
}
|
|
1141
|
-
|
|
1142
1419
|
const sourceValue = this.context[step.variable];
|
|
1143
|
-
|
|
1144
1420
|
try {
|
|
1145
1421
|
switch (this.dbClient.type) {
|
|
1146
1422
|
case 'postgres':
|
|
@@ -1157,16 +1433,14 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1157
1433
|
);
|
|
1158
1434
|
}
|
|
1159
1435
|
break;
|
|
1160
|
-
|
|
1161
1436
|
case 'mongodb':
|
|
1162
1437
|
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
1163
1438
|
await db.collection(step.collection).insertOne({
|
|
1164
1439
|
workflow_name: this.context.workflow_name || 'unknown',
|
|
1165
|
-
data: sourceValue,
|
|
1440
|
+
data: sourceValue,
|
|
1166
1441
|
created_at: new Date()
|
|
1167
1442
|
});
|
|
1168
1443
|
break;
|
|
1169
|
-
|
|
1170
1444
|
case 'sqlite':
|
|
1171
1445
|
const stmt = this.dbClient.client.prepare(
|
|
1172
1446
|
`INSERT INTO ${step.collection} (workflow_name, data, created_at) VALUES (?, ?, ?)`
|
|
@@ -1178,7 +1452,6 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1178
1452
|
);
|
|
1179
1453
|
break;
|
|
1180
1454
|
}
|
|
1181
|
-
|
|
1182
1455
|
if (this.verbose) {
|
|
1183
1456
|
console.log(`🗄️ Persisted "${step.variable}" to DB collection ${step.collection}`);
|
|
1184
1457
|
}
|
|
@@ -1199,22 +1472,41 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1199
1472
|
if (workflow.type !== 'workflow') {
|
|
1200
1473
|
throw new Error(`Unknown workflow type: ${workflow.type}`);
|
|
1201
1474
|
}
|
|
1202
|
-
|
|
1203
|
-
this.context = {
|
|
1204
|
-
...inputs,
|
|
1205
|
-
workflow_name: workflow.name
|
|
1475
|
+
|
|
1476
|
+
this.context = {
|
|
1477
|
+
...inputs,
|
|
1478
|
+
workflow_name: workflow.name
|
|
1206
1479
|
};
|
|
1207
1480
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1481
|
+
// ✅ AUDIT LOG: Workflow start (ENHANCED with governance metadata)
|
|
1482
|
+
const governanceHash = this._generateGovernanceProfileHash(workflow);
|
|
1483
|
+
|
|
1484
|
+
this._createAuditEntry('workflow_started', {
|
|
1485
|
+
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`, // ✅ Workflow ID
|
|
1486
|
+
workflow_name: workflow.name,
|
|
1487
|
+
workflow_version: workflow.version || null,
|
|
1488
|
+
kernel_version: KERNEL_VERSION, // ✅ Kernel Version
|
|
1489
|
+
runtime_version: KERNEL_VERSION, // ✅ Runtime Version (same as kernel for now)
|
|
1490
|
+
governance_profile_hash: governanceHash, // ✅ Governance Profile Hash
|
|
1491
|
+
inputs_count: Object.keys(inputs).length,
|
|
1492
|
+
steps_count: workflow.steps.length,
|
|
1493
|
+
allowed_resolvers: workflow.allowedResolvers || [],
|
|
1494
|
+
constraints: {
|
|
1495
|
+
max_generations: workflow.maxGenerations,
|
|
1496
|
+
strict_inputs: process.env.OLANG_STRICT_INPUTS === 'true'
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
// Optional strict mode: enforce resolver-originated inputs
|
|
1501
|
+
if (process.env.OLANG_STRICT_INPUTS === 'true') {
|
|
1502
|
+
if (!inputs.__resolver_origin) {
|
|
1503
|
+
throw new Error(
|
|
1504
|
+
'[O-Lang SAFETY] Inputs must originate from a certified resolver. ' +
|
|
1505
|
+
'Use @o-lang/input-validator to validate external data.'
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1215
1508
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1509
|
+
|
|
1218
1510
|
const currentGeneration = inputs.__generation || 1;
|
|
1219
1511
|
if (workflow.maxGenerations !== null && currentGeneration > workflow.maxGenerations) {
|
|
1220
1512
|
throw new Error(`Workflow generation ${currentGeneration} exceeds Constraint: max_generations = ${workflow.maxGenerations}`);
|
|
@@ -1222,10 +1514,8 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1222
1514
|
|
|
1223
1515
|
this.workflowSteps = workflow.steps;
|
|
1224
1516
|
this.allowedResolvers = new Set(workflow.allowedResolvers || []);
|
|
1225
|
-
|
|
1226
1517
|
const mathPattern =
|
|
1227
1518
|
/^(Add|Subtract|Multiply|Divide|Sum|Avg|Min|Max|Round|Floor|Ceil|Abs)\b/i;
|
|
1228
|
-
|
|
1229
1519
|
for (const step of workflow.steps) {
|
|
1230
1520
|
if (step.type === 'calculate' || (step.actionRaw && mathPattern.test(step.actionRaw))) {
|
|
1231
1521
|
this.allowedResolvers.add('builtInMathResolver');
|
|
@@ -1235,9 +1525,23 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1235
1525
|
for (const step of workflow.steps) {
|
|
1236
1526
|
await this.executeStep(step, agentResolver);
|
|
1237
1527
|
}
|
|
1528
|
+
|
|
1529
|
+
// ✅ AUDIT LOG: Workflow completion (ENHANCED)
|
|
1530
|
+
this._createAuditEntry('workflow_completed', {
|
|
1531
|
+
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`,
|
|
1532
|
+
workflow_name: workflow.name,
|
|
1533
|
+
kernel_version: KERNEL_VERSION,
|
|
1534
|
+
runtime_version: KERNEL_VERSION,
|
|
1535
|
+
governance_profile_hash: governanceHash,
|
|
1536
|
+
return_values: workflow.returnValues,
|
|
1537
|
+
total_steps: workflow.steps.length,
|
|
1538
|
+
execution_summary: {
|
|
1539
|
+
warnings: this.__warnings.length,
|
|
1540
|
+
disallowed_attempts: this.disallowedAttempts.length
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1238
1543
|
|
|
1239
1544
|
this.printDisallowedSummary();
|
|
1240
|
-
|
|
1241
1545
|
if (this.__warnings.length) {
|
|
1242
1546
|
console.log(`\n[O-Lang] ⚠️ Parser/Runtime Warnings (${this.__warnings.length}):`);
|
|
1243
1547
|
this.__warnings.slice(0, 5).forEach((w, i) => {
|