@o-lang/olang 1.2.14 → 1.2.16
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 +736 -470
- package/src/runtime/index.js +1 -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,162 @@ 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
724
|
// -----------------------------
|
|
335
|
-
// ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION (CONJUGATION-AWARE + EVASION-RESISTANT)
|
|
336
|
-
// -----------------------------
|
|
337
|
-
_validateLLMOutput(output, actionContext) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
// ────────────────────────────────────────────────
|
|
466
|
-
|
|
467
|
-
{ 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' }
|
|
468
|
-
];
|
|
469
|
-
|
|
470
|
-
// 🔍 SCAN OUTPUT FOR FORBIDDEN INTENTS (financial/PII/fake confirmations)
|
|
471
|
-
for (const { pattern, capability, lang } of forbiddenPatterns) {
|
|
472
|
-
if (pattern.test(output)) {
|
|
473
|
-
// ✅ Only block if capability NOT in workflow allowlist
|
|
474
|
-
const hasCapability = allowedCapabilities.some(c =>
|
|
475
|
-
c.includes(capability) ||
|
|
476
|
-
c.includes('transfer') ||
|
|
477
|
-
c.includes('payment') ||
|
|
478
|
-
c.includes('financial') ||
|
|
479
|
-
c.includes('deposit') ||
|
|
480
|
-
c.includes('withdraw')
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
if (!hasCapability) {
|
|
484
|
-
const match = output.match(pattern);
|
|
485
|
-
return {
|
|
486
|
-
passed: false,
|
|
487
|
-
reason: `Hallucinated "${capability}" capability in ${lang} (not in workflow allowlist: ${allowedCapabilities.join(', ') || 'none'})`,
|
|
488
|
-
detected: match ? match[0].trim() : 'unknown pattern',
|
|
489
|
-
language: lang
|
|
490
|
-
};
|
|
725
|
+
// ✅ KERNEL-LEVEL LLM HALLUCINATION PREVENTION (CONJUGATION-AWARE + EVASION-RESISTANT)
|
|
726
|
+
// -----------------------------
|
|
727
|
+
_validateLLMOutput(output, actionContext) {
|
|
728
|
+
if (!output || typeof output !== 'string') return { passed: true };
|
|
729
|
+
|
|
730
|
+
// ── __verified_intent takes priority ──────────────────────────────────────
|
|
731
|
+
// If the workflow author has defined intent rules, use those exclusively.
|
|
732
|
+
// This makes governance dynamic — skip hardcoded patterns entirely.
|
|
733
|
+
const intent = this.context.__verified_intent;
|
|
734
|
+
if (intent) {
|
|
735
|
+
if (intent.prohibited_actions && Array.isArray(intent.prohibited_actions)) {
|
|
736
|
+
const lower = output.toLowerCase();
|
|
737
|
+
for (const action of intent.prohibited_actions) {
|
|
738
|
+
if (lower.includes(action.toLowerCase())) {
|
|
739
|
+
return {
|
|
740
|
+
passed: false,
|
|
741
|
+
reason: `Output violates prohibited action "${action}" defined in __verified_intent`,
|
|
742
|
+
detected: action,
|
|
743
|
+
language: 'multi'
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
491
747
|
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
748
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
detected: topic,
|
|
507
|
-
language: 'multi'
|
|
508
|
-
};
|
|
749
|
+
if (intent.prohibited_topics && Array.isArray(intent.prohibited_topics)) {
|
|
750
|
+
const lower = output.toLowerCase();
|
|
751
|
+
for (const topic of intent.prohibited_topics) {
|
|
752
|
+
if (lower.includes(topic.toLowerCase())) {
|
|
753
|
+
return {
|
|
754
|
+
passed: false,
|
|
755
|
+
reason: `Output violates prohibited topic "${topic}" defined in __verified_intent`,
|
|
756
|
+
detected: topic,
|
|
757
|
+
language: 'multi'
|
|
758
|
+
};
|
|
759
|
+
}
|
|
509
760
|
}
|
|
510
761
|
}
|
|
762
|
+
|
|
763
|
+
// __verified_intent present and passed — skip hardcoded patterns
|
|
764
|
+
return { passed: true };
|
|
511
765
|
}
|
|
512
766
|
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
767
|
+
// ── No __verified_intent — fall through to hardcoded patterns ─────────────
|
|
768
|
+
// 🔑 Extract allowed capabilities from workflow allowlist
|
|
769
|
+
const allowedCapabilities = Array.from(this.allowedResolvers)
|
|
770
|
+
.filter(name => !name.startsWith('llm-') && name !== 'builtInMathResolver')
|
|
771
|
+
.map(name => name.replace('@o-lang/', '').replace(/-resolver$/, ''));
|
|
772
|
+
|
|
773
|
+
// 🔒 CONJUGATION-AWARE + EVASION-RESISTANT PAN-AFRICAN INTENT DETECTION
|
|
774
|
+
const forbiddenPatterns = [
|
|
775
|
+
// ────────────────────────────────────────────────
|
|
776
|
+
// 🇳🇬 NIGERIAN LANGUAGES (Conjugation-aware)
|
|
777
|
+
// ────────────────────────────────────────────────
|
|
778
|
+
{ pattern: /\bti\s+(?:fi|san|gba|da|lo)\b/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
779
|
+
{ pattern: /\b(?:ń|ǹ|n)\s+(?:fi|san|gba)\b/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
780
|
+
{ pattern: /\b(fi\s+(?:owo|ẹ̀wọ̀|ewo|ku|fun|s'ọkọọ))\b/i, capability: 'transfer', lang: 'yo' },
|
|
781
|
+
{ pattern: /\b(san\s+(?:owo|ẹ̀wọ̀|ewo|fun|wo))\b/i, capability: 'payment', lang: 'yo' },
|
|
782
|
+
{ pattern: /\b(gba\s+owo)\b/i, capability: 'withdrawal', lang: 'yo' },
|
|
783
|
+
{ pattern: /\b(mo\s+ti\s+(?:fi|san|gba))\b/i, capability: 'unauthorized_action', lang: 'yo' },
|
|
784
|
+
{ pattern: /\b(?:ya|ta|su)\s+(?:ciyar|biya|sahawa|sake)\b/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
785
|
+
{ pattern: /\b(?:za\sa|za\s+ta)\s+(?:ciyar|biya)\b/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
786
|
+
{ pattern: /\b(ciyar\s*(?:da)?|ciya\s*(?:da)?|shiga\s+kuɗi)\b/i, capability: 'transfer', lang: 'ha' },
|
|
787
|
+
{ pattern: /\b(biya\s*(?:da)?)\b/i, capability: 'payment', lang: 'ha' },
|
|
788
|
+
{ pattern: /\b(sahaw[ae]\s+kuɗi)\b/i, capability: 'withdrawal', lang: 'ha' },
|
|
789
|
+
{ pattern: /\b(ina\s+(?:ciyar|biya|sahawa))\b/i, capability: 'unauthorized_action', lang: 'ha' },
|
|
790
|
+
{ pattern: /\b(?:ziri|bururu|tinyere|gbara)\b/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
791
|
+
{ pattern: /\b(zipu\s+(?:ego|moni|isi|na))\b/i, capability: 'transfer', lang: 'ig' },
|
|
792
|
+
{ pattern: /\b(buru\s+(?:ego|moni|isi))\b/i, capability: 'transfer', lang: 'ig' },
|
|
793
|
+
{ pattern: /\b(tinye\s+(?:ego|moni|isi))\b/i, capability: 'deposit', lang: 'ig' },
|
|
794
|
+
{ pattern: /\b(m\s+(?:ziri|buru|zipuru|tinyere))\b/i, capability: 'unauthorized_action', lang: 'ig' },
|
|
795
|
+
{ 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' },
|
|
796
|
+
{ 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' },
|
|
797
|
+
{ 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' },
|
|
798
|
+
{ 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' },
|
|
799
|
+
{ pattern: /\b(tuma\s+(?:pesa|fedha)|pelek[ae]?\s+(?:pesa|fedha)|wasilisha)\b/i, capability: 'transfer', lang: 'sw' },
|
|
800
|
+
{ pattern: /\b(lipa|maliza\s+malipo)\b/i, capability: 'payment', lang: 'sw' },
|
|
801
|
+
{ pattern: /\b(ongez[ae]?\s*(?:kiasi|pesa|fedha)|wek[ae]?\s+(?:katika|ndani)\s+(?:akaunti|hisa))\b/i, capability: 'deposit', lang: 'sw' },
|
|
802
|
+
{ pattern: /\b(nime(?:tuma|lipa|ongeza|weka|peleka))\b/i, capability: 'unauthorized_action', lang: 'sw' },
|
|
803
|
+
{ pattern: /[\u1200-\u137F]{0,4}(?:ተላላፈ|ላክ|ክፈል|ጨምር|ወጣ|ገባ)[\u1200-\u137F]{0,2}(?:\u1205|\u122d|\u1265)[\u1200-\u137F]{0,2}/u, capability: 'financial_action', lang: 'am' },
|
|
804
|
+
{ pattern: /\bni\s+(?:kuufe|dhiibe|kennine|gurgure)\b/i, capability: 'unauthorized_action', lang: 'om' },
|
|
805
|
+
{ pattern: /\b(kuuf\s+(?:qilleensaa|bilbila)|dhiib\s+(?:qilleensaa|bilbila))\b/i, capability: 'transfer', lang: 'om' },
|
|
806
|
+
{ pattern: /\b(kenn\s*i|gurgur\s*i)\b/i, capability: 'payment', lang: 'om' },
|
|
807
|
+
{ pattern: /\b(sakkit\s+(?:ndo|ndoo)|tawt\s+(?:ndo|ndoo))\b/i, capability: 'transfer', lang: 'ff' },
|
|
808
|
+
{ pattern: /\b(jokk\s*i|soodug\s*i)\b/i, capability: 'payment', lang: 'ff' },
|
|
809
|
+
{ pattern: /\bwaxaa\s+(?:diray|bixiyay|ku\s+daray|sameeyay)\b/i, capability: 'unauthorized_action', lang: 'so' },
|
|
810
|
+
{ pattern: /\b(dir\s+(?:lacag|maal|qarsoon))\b/i, capability: 'transfer', lang: 'so' },
|
|
811
|
+
{ pattern: /\b(bixi|bixis\s*o)\b/i, capability: 'payment', lang: 'so' },
|
|
812
|
+
{ pattern: /\b(?:thumel|hlawul|fik)\s*ile\b/i, capability: 'unauthorized_action', lang: 'zu' },
|
|
813
|
+
{ pattern: /\b(thumel\s*a\s+(?:imali|imali))\b/i, capability: 'transfer', lang: 'zu' },
|
|
814
|
+
{ pattern: /\b(hlawul\s*a|hlawulel\s*a)\b/i, capability: 'payment', lang: 'zu' },
|
|
815
|
+
{ pattern: /\b(siyithumel\s*e|siyihlawul\s*e)\b/i, capability: 'unauthorized_action', lang: 'zu' },
|
|
816
|
+
{ pattern: /\b(?:tumir|bhadhar)\s*a\b/i, capability: 'unauthorized_action', lang: 'sn' },
|
|
817
|
+
{ pattern: /\b(tumir\s*a\s+(?:mhando|ari))\b/i, capability: 'transfer', lang: 'sn' },
|
|
818
|
+
{ pattern: /\b(bhadhara|bhadharis\s*o)\b/i, capability: 'payment', lang: 'sn' },
|
|
819
|
+
// ────────────────────────────────────────────────
|
|
820
|
+
// 🌐 GLOBAL LANGUAGES
|
|
821
|
+
// ────────────────────────────────────────────────
|
|
822
|
+
{ pattern: /\b(?:have|has|had)\s+(?:transferred|sent|paid|withdrawn|deposited|wire[d])\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
823
|
+
{ pattern: /\b(?:was|were|been)\s+(?:added|credited|transferred|sent|paid)\b/i, capability: 'unauthorized_action', lang: 'en' },
|
|
824
|
+
{ 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' },
|
|
825
|
+
{ 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' },
|
|
826
|
+
{ 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' },
|
|
827
|
+
{ pattern: /\b(virer|transférer|envoyer|payer|retirer|déposer|débiter|créditer)\b/i, capability: 'financial_action', lang: 'fr' },
|
|
828
|
+
{ pattern: /[\u0600-\u06FF]{0,3}(?:حوّل|أرسل|ادفع|اودع|سحب)[\u0600-\u06FF]{0,3}(?:ت|نا|تم|تا|تِ|تُ|تَ)[\u0600-\u06FF]{0,3}/u, capability: 'financial_action', lang: 'ar' },
|
|
829
|
+
{ pattern: /[\u0600-\u06FF]{0,3}(?:أنا|تم|لقد)\s*(?:حوّلت|أرسلت|دفعت|اودعت)[\u0600-\u06FF]{0,3}/u, capability: 'unauthorized_action', lang: 'ar' },
|
|
830
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账 | 支付 | 存款 | 取款)[\u4e00-\u9fff]{0,2}(?:了)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
831
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:转账 | 转帐 | 支付 | 付款 | 提款 | 取款 | 存款 | 存入 | 汇款 | 存)[\u4e00-\u9fff]{0,2}/u, capability: 'financial_action', lang: 'zh' },
|
|
832
|
+
{ pattern: /[\u4e00-\u9fff]{0,2}(?:我 | 已 | 已经)\s*(?:转账 | 支付 | 提款 | 存款)[\u4e00-\u9fff]{0,2}/u, capability: 'unauthorized_action', lang: 'zh' },
|
|
833
|
+
// ────────────────────────────────────────────────
|
|
834
|
+
// 🛡️ EVASION-RESISTANT + PII + FAKE CONFIRMATION
|
|
835
|
+
// ────────────────────────────────────────────────
|
|
836
|
+
{ 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, capability: 'unauthorized_action', lang: 'multi' },
|
|
837
|
+
{ 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' },
|
|
838
|
+
{ pattern: /\b(?:bvn|bank verification number)\s*[:\-]?\s*(\d{11})\b/i, capability: 'pii_exposure', lang: 'multi' },
|
|
839
|
+
{ pattern: /\b(?:\+?234\s*|0)(?:70|80|81|90|91)\d{8}\b/, capability: 'pii_exposure', lang: 'multi' },
|
|
840
|
+
{ 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' },
|
|
841
|
+
];
|
|
842
|
+
|
|
843
|
+
// 🔍 SCAN OUTPUT FOR FORBIDDEN INTENTS
|
|
844
|
+
for (const { pattern, capability, lang } of forbiddenPatterns) {
|
|
845
|
+
if (pattern.test(output)) {
|
|
846
|
+
const hasCapability = allowedCapabilities.some(c =>
|
|
847
|
+
c.includes(capability) ||
|
|
848
|
+
c.includes('transfer') ||
|
|
849
|
+
c.includes('payment') ||
|
|
850
|
+
c.includes('financial') ||
|
|
851
|
+
c.includes('deposit') ||
|
|
852
|
+
c.includes('withdraw')
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
if (!hasCapability) {
|
|
856
|
+
const match = output.match(pattern);
|
|
518
857
|
return {
|
|
519
858
|
passed: false,
|
|
520
|
-
reason: `
|
|
521
|
-
detected:
|
|
522
|
-
language:
|
|
859
|
+
reason: `Hallucinated "${capability}" capability in ${lang} (not in workflow allowlist: ${allowedCapabilities.join(', ') || 'none'})`,
|
|
860
|
+
detected: match ? match[0].trim() : 'unknown pattern',
|
|
861
|
+
language: lang
|
|
523
862
|
};
|
|
524
863
|
}
|
|
525
864
|
}
|
|
526
865
|
}
|
|
527
|
-
}
|
|
528
866
|
|
|
529
|
-
|
|
530
|
-
}
|
|
867
|
+
return { passed: true };
|
|
868
|
+
}
|
|
531
869
|
// -----------------------------
|
|
532
870
|
// ✅ CRITICAL FIX: Resolver output unwrapping helper
|
|
533
871
|
// -----------------------------
|
|
534
872
|
_unwrapResolverResult(result) {
|
|
535
|
-
// Standard O-Lang resolver contract: { output: {...} } or { error: "..." }
|
|
536
873
|
if (result && typeof result === 'object' && 'output' in result && result.output !== undefined) {
|
|
537
874
|
return result.output;
|
|
538
875
|
}
|
|
539
|
-
// Legacy resolvers might return raw values
|
|
540
876
|
return result;
|
|
541
877
|
}
|
|
542
878
|
|
|
@@ -545,11 +881,10 @@ _validateLLMOutput(output, actionContext) {
|
|
|
545
881
|
// -----------------------------
|
|
546
882
|
async executeStep(step, agentResolver) {
|
|
547
883
|
const stepType = step.type;
|
|
548
|
-
|
|
884
|
+
|
|
549
885
|
// ✅ Enforce per-step constraints (basic validation)
|
|
550
886
|
if (step.constraints && Object.keys(step.constraints).length > 0) {
|
|
551
887
|
for (const [key, value] of Object.entries(step.constraints)) {
|
|
552
|
-
// Log unsupported constraints (future extensibility)
|
|
553
888
|
if (['max_time_sec', 'cost_limit', 'allowed_resolvers'].includes(key)) {
|
|
554
889
|
this.addWarning(`Per-step constraint "${key}=${value}" is parsed but not yet enforced`);
|
|
555
890
|
} else {
|
|
@@ -561,19 +896,15 @@ _validateLLMOutput(output, actionContext) {
|
|
|
561
896
|
// ✅ ADDITION 3 — Resolver Policy Enforcement (External + Local)
|
|
562
897
|
const enforceResolverPolicy = (resolver, step) => {
|
|
563
898
|
const resolverName = resolver?.resolverName || resolver?.name;
|
|
564
|
-
|
|
565
899
|
if (!resolverName) {
|
|
566
900
|
throw new Error('[O-Lang] Resolver missing resolverName');
|
|
567
901
|
}
|
|
568
|
-
|
|
569
902
|
if (!this.allowedResolvers.has(resolverName)) {
|
|
570
903
|
this.logDisallowedResolver(resolverName, step.actionRaw || step.type);
|
|
571
904
|
throw new Error(
|
|
572
905
|
`[O-Lang] Resolver "${resolverName}" blocked by workflow policy`
|
|
573
906
|
);
|
|
574
907
|
}
|
|
575
|
-
|
|
576
|
-
// External resolvers MUST be HTTP-only
|
|
577
908
|
if (this._isExternalResolver(resolver)) {
|
|
578
909
|
if (!resolver.manifest.endpoint) {
|
|
579
910
|
throw new Error(
|
|
@@ -583,11 +914,10 @@ _validateLLMOutput(output, actionContext) {
|
|
|
583
914
|
}
|
|
584
915
|
};
|
|
585
916
|
|
|
586
|
-
// ✅ CORRECTED: Strict safety WITH dynamic diagnostics
|
|
917
|
+
// ✅ CORRECTED: Strict safety WITH dynamic diagnostics
|
|
587
918
|
const runResolvers = async (action) => {
|
|
588
919
|
const mathPattern =
|
|
589
920
|
/^(Add|Subtract|Multiply|Divide|Sum|Avg|Min|Max|Round|Floor|Ceil|Abs)\b/i;
|
|
590
|
-
|
|
591
921
|
if (
|
|
592
922
|
step.actionRaw &&
|
|
593
923
|
mathPattern.test(step.actionRaw) &&
|
|
@@ -596,9 +926,7 @@ _validateLLMOutput(output, actionContext) {
|
|
|
596
926
|
this.allowedResolvers.add('builtInMathResolver');
|
|
597
927
|
}
|
|
598
928
|
|
|
599
|
-
// Handle different resolver input formats
|
|
600
929
|
let resolversToRun = [];
|
|
601
|
-
|
|
602
930
|
if (agentResolver && Array.isArray(agentResolver._chain)) {
|
|
603
931
|
resolversToRun = agentResolver._chain;
|
|
604
932
|
} else if (Array.isArray(agentResolver)) {
|
|
@@ -607,9 +935,7 @@ _validateLLMOutput(output, actionContext) {
|
|
|
607
935
|
resolversToRun = [agentResolver];
|
|
608
936
|
}
|
|
609
937
|
|
|
610
|
-
// ✅ Track detailed resolver outcomes for diagnostics
|
|
611
938
|
const resolverAttempts = [];
|
|
612
|
-
|
|
613
939
|
for (let idx = 0; idx < resolversToRun.length; idx++) {
|
|
614
940
|
const resolver = resolversToRun[idx];
|
|
615
941
|
const resolverName = resolver?.resolverName || resolver?.name || `resolver-${idx}`;
|
|
@@ -617,7 +943,6 @@ _validateLLMOutput(output, actionContext) {
|
|
|
617
943
|
|
|
618
944
|
try {
|
|
619
945
|
let result;
|
|
620
|
-
|
|
621
946
|
if (this._isExternalResolver(resolver)) {
|
|
622
947
|
result = await this._callExternalResolver(
|
|
623
948
|
resolver,
|
|
@@ -628,47 +953,35 @@ _validateLLMOutput(output, actionContext) {
|
|
|
628
953
|
result = await resolver(action, this.context);
|
|
629
954
|
}
|
|
630
955
|
|
|
631
|
-
// ✅ ACCEPT valid result immediately (non-null/non-undefined)
|
|
632
956
|
if (result !== undefined && result !== null) {
|
|
633
|
-
// ✅ CRITICAL FIX: Save raw result for debugging (like __resolver_0)
|
|
634
957
|
this.context[`__resolver_${idx}`] = result;
|
|
635
|
-
|
|
636
|
-
// ✅ UNWRAP before returning to workflow logic
|
|
637
958
|
return this._unwrapResolverResult(result);
|
|
638
959
|
}
|
|
639
|
-
|
|
640
|
-
// ⚪ Resolver skipped this action (normal behavior)
|
|
960
|
+
|
|
641
961
|
resolverAttempts.push({
|
|
642
962
|
name: resolverName,
|
|
643
963
|
status: 'skipped',
|
|
644
964
|
reason: 'Action not recognized'
|
|
645
965
|
});
|
|
646
|
-
|
|
647
966
|
} catch (e) {
|
|
648
|
-
// ❌ Resolver attempted but failed — capture structured diagnostics
|
|
649
967
|
const diagnostics = {
|
|
650
968
|
error: e.message || String(e),
|
|
651
|
-
requiredEnvVars: e.requiredEnvVars || [],
|
|
652
|
-
missingInputs: e.missingInputs || [],
|
|
653
|
-
documentationUrl: resolver?.documentationUrl ||
|
|
654
|
-
|
|
969
|
+
requiredEnvVars: e.requiredEnvVars || [],
|
|
970
|
+
missingInputs: e.missingInputs || [],
|
|
971
|
+
documentationUrl: resolver?.documentationUrl ||
|
|
972
|
+
(resolver?.manifest?.documentationUrl) || null
|
|
655
973
|
};
|
|
656
|
-
|
|
657
974
|
resolverAttempts.push({
|
|
658
975
|
name: resolverName,
|
|
659
976
|
status: 'failed',
|
|
660
977
|
diagnostics
|
|
661
978
|
});
|
|
662
|
-
|
|
663
|
-
// Log for verbose mode but continue chaining
|
|
664
979
|
this.addWarning(`Resolver "${resolverName}" failed for action "${action}": ${diagnostics.error}`);
|
|
665
980
|
}
|
|
666
981
|
}
|
|
667
982
|
|
|
668
|
-
|
|
669
|
-
let errorMessage = `[O-Lang SAFETY] No resolver handled action: "${action}"\n\n`;
|
|
983
|
+
let errorMessage = `[O-Lang SAFETY] No resolver handled action: "${action}\n`;
|
|
670
984
|
errorMessage += `Attempted resolvers:\n`;
|
|
671
|
-
|
|
672
985
|
resolverAttempts.forEach((attempt, i) => {
|
|
673
986
|
const namePad = attempt.name.padEnd(30);
|
|
674
987
|
if (attempt.status === 'skipped') {
|
|
@@ -676,25 +989,18 @@ _validateLLMOutput(output, actionContext) {
|
|
|
676
989
|
} else {
|
|
677
990
|
errorMessage += ` ${i + 1}. ${namePad} → FAILED\n`;
|
|
678
991
|
errorMessage += ` Error: ${attempt.diagnostics.error}\n`;
|
|
679
|
-
|
|
680
|
-
// ✅ DYNAMIC HINT: Resolver-provided env vars
|
|
681
992
|
if (attempt.diagnostics.requiredEnvVars?.length) {
|
|
682
993
|
errorMessage += ` Required env vars: ${attempt.diagnostics.requiredEnvVars.join(', ')}\n`;
|
|
683
994
|
}
|
|
684
|
-
|
|
685
|
-
// ✅ DYNAMIC HINT: Resolver-provided docs link
|
|
686
995
|
if (attempt.diagnostics.documentationUrl) {
|
|
687
996
|
errorMessage += ` Docs: ${attempt.diagnostics.documentationUrl}\n`;
|
|
688
997
|
}
|
|
689
998
|
}
|
|
690
999
|
});
|
|
691
1000
|
|
|
692
|
-
// ✅ ACCURATE REMEDIATION (NO OBSOLETE "REMOVE ACTION KEYWORD" HINT)
|
|
693
1001
|
const failed = resolverAttempts.filter(a => a.status === 'failed');
|
|
694
1002
|
const allSkipped = failed.length === 0;
|
|
695
|
-
|
|
696
1003
|
errorMessage += `\n💡 How to fix:\n`;
|
|
697
|
-
|
|
698
1004
|
if (allSkipped) {
|
|
699
1005
|
errorMessage += ` • Verify the action matches a resolver's capabilities:\n`;
|
|
700
1006
|
errorMessage += ` → Check resolver documentation for supported actions\n`;
|
|
@@ -705,27 +1011,22 @@ _validateLLMOutput(output, actionContext) {
|
|
|
705
1011
|
errorMessage += ` → Set required environment variables (if listed)\n`;
|
|
706
1012
|
errorMessage += ` → Verify inputs exist in workflow context\n`;
|
|
707
1013
|
errorMessage += ` → Check resolver documentation for requirements\n`;
|
|
708
|
-
|
|
709
|
-
// Pattern-based hints (generic, not hardcoded)
|
|
710
1014
|
const envVarPattern = /environment variable|env\.|process\.env|missing.*path/i;
|
|
711
1015
|
if (failed.some(f => envVarPattern.test(f.diagnostics.error))) {
|
|
712
1016
|
errorMessage += ` → Example (PowerShell): $env:VARIABLE="value"\n`;
|
|
713
1017
|
errorMessage += ` → Example (Linux/macOS): export VARIABLE="value"\n`;
|
|
714
1018
|
}
|
|
715
|
-
|
|
716
1019
|
const dbPattern = /database|db\.|sqlite|postgres|mysql|mongodb/i;
|
|
717
1020
|
if (failed.some(f => dbPattern.test(f.diagnostics.error))) {
|
|
718
1021
|
errorMessage += ` → Ensure database file/connection exists and path is correct\n`;
|
|
719
1022
|
}
|
|
720
|
-
|
|
721
1023
|
const authPattern = /auth|api key|token|credential/i;
|
|
722
1024
|
if (failed.some(f => authPattern.test(f.diagnostics.error))) {
|
|
723
1025
|
errorMessage += ` → Verify API keys/tokens are set in environment variables\n`;
|
|
724
1026
|
}
|
|
725
1027
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
errorMessage += `\n • Resolver documentation:\n`;
|
|
1028
|
+
|
|
1029
|
+
errorMessage += `\n• Resolver documentation:\n`;
|
|
729
1030
|
let hasDocs = false;
|
|
730
1031
|
resolverAttempts.forEach(attempt => {
|
|
731
1032
|
if (attempt.diagnostics?.documentationUrl) {
|
|
@@ -734,9 +1035,8 @@ _validateLLMOutput(output, actionContext) {
|
|
|
734
1035
|
}
|
|
735
1036
|
});
|
|
736
1037
|
if (!hasDocs) {
|
|
737
|
-
errorMessage += ` → Visit https://www.npmjs.com/search?q=%40o-lang for resolver packages\n`;
|
|
1038
|
+
errorMessage += ` → Visit https://www.npmjs.com/search?q=%40o-lang for resolver packages\n`;
|
|
738
1039
|
}
|
|
739
|
-
|
|
740
1040
|
errorMessage += `\n🛑 Workflow halted to prevent unsafe data propagation to LLMs.`;
|
|
741
1041
|
throw new Error(errorMessage);
|
|
742
1042
|
};
|
|
@@ -747,161 +1047,143 @@ _validateLLMOutput(output, actionContext) {
|
|
|
747
1047
|
if (step.saveAs) this.context[step.saveAs] = result;
|
|
748
1048
|
break;
|
|
749
1049
|
}
|
|
1050
|
+
|
|
1051
|
+
case 'action': {
|
|
1052
|
+
let action = this._safeInterpolate(
|
|
1053
|
+
step.actionRaw,
|
|
1054
|
+
this.context,
|
|
1055
|
+
'action step'
|
|
1056
|
+
);
|
|
750
1057
|
|
|
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
|
-
}
|
|
1058
|
+
if (action.startsWith('Ask ')) {
|
|
1059
|
+
action = 'Action ' + action.slice(4);
|
|
1060
|
+
} else if (action.startsWith('Use ')) {
|
|
1061
|
+
action = 'Action ' + action.slice(4);
|
|
1062
|
+
}
|
|
774
1063
|
|
|
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
|
-
});
|
|
1064
|
+
if (!action.startsWith('Action ')) {
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`[O-Lang SAFETY] Non-canonical action received: "${action}\n` +
|
|
1067
|
+
` → Expected format: Action <resolver> <args>\n` +
|
|
1068
|
+
` → This indicates a kernel or workflow authoring error.`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
787
1071
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1072
|
+
const mathCall = action.match(
|
|
1073
|
+
/^(add|subtract|multiply|divide|sum|avg|min|max|round|floor|ceil|abs)\((.*)\)$/i
|
|
1074
|
+
);
|
|
1075
|
+
if (mathCall) {
|
|
1076
|
+
const fn = mathCall[1].toLowerCase();
|
|
1077
|
+
const args = mathCall[2].split(',').map(s => {
|
|
1078
|
+
s = s.trim();
|
|
1079
|
+
if (!isNaN(s)) return parseFloat(s);
|
|
1080
|
+
return this.getNested(this.context, s.replace(/^\{|\}$/g, ''));
|
|
1081
|
+
});
|
|
1082
|
+
if (this.mathFunctions[fn]) {
|
|
1083
|
+
const value = this.mathFunctions[fn](...args);
|
|
1084
|
+
if (step.saveAs) this.context[step.saveAs] = value;
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
794
1088
|
|
|
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
|
-
}
|
|
1089
|
+
const rawResult = await runResolvers(action);
|
|
1090
|
+
const unwrapped = this._unwrapResolverResult(rawResult);
|
|
852
1091
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1092
|
+
const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
1093
|
+
action.toLowerCase().includes('openai') ||
|
|
1094
|
+
action.toLowerCase().includes('anthropic') ||
|
|
1095
|
+
action.toLowerCase().includes('claude') ||
|
|
1096
|
+
action.toLowerCase().includes('gpt') ||
|
|
1097
|
+
action.toLowerCase().includes('gemini') ||
|
|
1098
|
+
action.toLowerCase().includes('google') ||
|
|
1099
|
+
action.toLowerCase().includes('llama') ||
|
|
1100
|
+
action.toLowerCase().includes('meta') ||
|
|
1101
|
+
action.toLowerCase().includes('mistral') ||
|
|
1102
|
+
action.toLowerCase().includes('mixtral') ||
|
|
1103
|
+
action.toLowerCase().includes('cohere') ||
|
|
1104
|
+
action.toLowerCase().includes('huggingface') ||
|
|
1105
|
+
action.toLowerCase().includes('hugging-face') ||
|
|
1106
|
+
action.toLowerCase().includes('together') ||
|
|
1107
|
+
action.toLowerCase().includes('perplexity') ||
|
|
1108
|
+
action.toLowerCase().includes('fireworks') ||
|
|
1109
|
+
action.toLowerCase().includes('bedrock') ||
|
|
1110
|
+
action.toLowerCase().includes('azure') ||
|
|
1111
|
+
action.toLowerCase().includes('ollama') ||
|
|
1112
|
+
action.toLowerCase().includes('replicate') ||
|
|
1113
|
+
action.toLowerCase().includes('deepseek') ||
|
|
1114
|
+
action.toLowerCase().includes('qwen') ||
|
|
1115
|
+
action.toLowerCase().includes('falcon') ||
|
|
1116
|
+
action.toLowerCase().includes('phi') ||
|
|
1117
|
+
action.toLowerCase().includes('gemma') ||
|
|
1118
|
+
action.toLowerCase().includes('stablelm') ||
|
|
1119
|
+
action.toLowerCase().includes('yi') ||
|
|
1120
|
+
action.toLowerCase().includes('dbrx') ||
|
|
1121
|
+
action.toLowerCase().includes('command') ||
|
|
1122
|
+
action.toLowerCase().includes('llm');
|
|
1123
|
+
|
|
1124
|
+
const llmText = unwrapped?.response ||
|
|
1125
|
+
unwrapped?.text ||
|
|
1126
|
+
unwrapped?.content ||
|
|
1127
|
+
unwrapped?.answer ||
|
|
1128
|
+
(typeof unwrapped === 'string' ? unwrapped : null);
|
|
1129
|
+
|
|
1130
|
+
if (isLLMAction && typeof llmText === 'string') {
|
|
1131
|
+
const safetyCheck = this._validateLLMOutput(llmText, action);
|
|
1132
|
+
if (!safetyCheck.passed) {
|
|
1133
|
+
throw new Error(
|
|
1134
|
+
`[O-Lang SAFETY] LLM hallucinated unauthorized capability:\n` +
|
|
1135
|
+
` → Detected: "${safetyCheck.detected}"\n` +
|
|
1136
|
+
` → Reason: ${safetyCheck.reason}\n` +
|
|
1137
|
+
` → Workflow allowlist: ${Array.from(this.allowedResolvers).join(', ')}\n` +
|
|
1138
|
+
`\n🛑 Halting to prevent deceptive user experience.`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
858
1142
|
|
|
1143
|
+
if (step.saveAs) {
|
|
1144
|
+
this.context[step.saveAs] = unwrapped;
|
|
1145
|
+
}
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
859
1149
|
case 'use': {
|
|
860
|
-
// ✅ SAFE INTERPOLATION for tool name
|
|
861
1150
|
const tool = this._safeInterpolate(step.tool, this.context, 'tool name');
|
|
862
1151
|
const rawResult = await runResolvers(`Use ${tool}`);
|
|
863
1152
|
const unwrapped = this._unwrapResolverResult(rawResult);
|
|
864
|
-
|
|
865
1153
|
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
866
1154
|
break;
|
|
867
1155
|
}
|
|
1156
|
+
|
|
1157
|
+
case 'ask': {
|
|
1158
|
+
const target = this._safeInterpolate(step.target, this.context, 'LLM prompt');
|
|
1159
|
+
if (/{[^}]+}/.test(target)) {
|
|
1160
|
+
throw new Error(`[O-Lang] Unresolved variables in prompt: "${target}"`);
|
|
1161
|
+
}
|
|
1162
|
+
const rawResult = await runResolvers(`Action ${target}`);
|
|
1163
|
+
const unwrapped = this._unwrapResolverResult(rawResult);
|
|
868
1164
|
|
|
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
|
-
}
|
|
1165
|
+
if (typeof unwrapped?.output === 'string') {
|
|
1166
|
+
const safetyCheck = this._validateLLMOutput(unwrapped.output, target);
|
|
1167
|
+
if (!safetyCheck.passed) {
|
|
1168
|
+
throw new Error(
|
|
1169
|
+
`[O-Lang SAFETY] LLM hallucinated unauthorized capability:\n` +
|
|
1170
|
+
` → Detected: "${safetyCheck.detected}"\n` +
|
|
1171
|
+
` → Reason: ${safetyCheck.reason}\n` +
|
|
1172
|
+
` → Workflow allowlist: ${Array.from(this.allowedResolvers).join(', ')}\n` +
|
|
1173
|
+
`\n🛑 Halting to prevent deceptive user experience.`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
897
1177
|
|
|
1178
|
+
if (step.saveAs) this.context[step.saveAs] = unwrapped;
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
898
1182
|
case 'evolve': {
|
|
899
1183
|
const { targetResolver, feedback } = step;
|
|
900
|
-
|
|
901
1184
|
if (this.verbose) {
|
|
902
1185
|
console.log(`🔄 Evolve step: ${targetResolver} with feedback: "${feedback}"`);
|
|
903
1186
|
}
|
|
904
|
-
|
|
905
1187
|
const evolutionResult = {
|
|
906
1188
|
resolver: targetResolver,
|
|
907
1189
|
feedback: feedback,
|
|
@@ -909,41 +1191,34 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
909
1191
|
timestamp: new Date().toISOString(),
|
|
910
1192
|
workflow: this.context.workflow_name
|
|
911
1193
|
};
|
|
912
|
-
|
|
913
1194
|
if (process.env.OLANG_EVOLUTION_API_KEY) {
|
|
914
1195
|
evolutionResult.status = 'advanced_evolution_enabled';
|
|
915
1196
|
evolutionResult.message = 'Advanced evolution service would process this request';
|
|
916
1197
|
}
|
|
917
|
-
|
|
918
1198
|
if (step.saveAs) {
|
|
919
1199
|
this.context[step.saveAs] = evolutionResult;
|
|
920
1200
|
}
|
|
921
1201
|
break;
|
|
922
1202
|
}
|
|
923
|
-
|
|
1203
|
+
|
|
924
1204
|
case 'if': {
|
|
925
1205
|
if (this.evaluateCondition(step.condition, this.context)) {
|
|
926
1206
|
for (const s of step.body) await this.executeStep(s, agentResolver);
|
|
927
1207
|
}
|
|
928
1208
|
break;
|
|
929
1209
|
}
|
|
930
|
-
|
|
1210
|
+
|
|
931
1211
|
case 'parallel': {
|
|
932
1212
|
const { steps, timeout } = step;
|
|
933
|
-
|
|
934
1213
|
if (timeout !== undefined && timeout > 0) {
|
|
935
|
-
// Timed parallel execution
|
|
936
1214
|
const timeoutPromise = new Promise(resolve => {
|
|
937
1215
|
setTimeout(() => resolve({ timedOut: true }), timeout);
|
|
938
1216
|
});
|
|
939
|
-
|
|
940
1217
|
const parallelPromise = Promise.all(
|
|
941
1218
|
steps.map(s => this.executeStep(s, agentResolver))
|
|
942
1219
|
).then(() => ({ timedOut: false }));
|
|
943
|
-
|
|
944
1220
|
const result = await Promise.race([timeoutPromise, parallelPromise]);
|
|
945
1221
|
this.context.timed_out = result.timedOut;
|
|
946
|
-
|
|
947
1222
|
if (result.timedOut) {
|
|
948
1223
|
this.emit('parallel_timeout', { duration: timeout, steps: steps.length });
|
|
949
1224
|
if (this.verbose) {
|
|
@@ -951,28 +1226,23 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
951
1226
|
}
|
|
952
1227
|
}
|
|
953
1228
|
} else {
|
|
954
|
-
// Normal parallel execution (no timeout)
|
|
955
1229
|
await Promise.all(steps.map(s => this.executeStep(s, agentResolver)));
|
|
956
1230
|
this.context.timed_out = false;
|
|
957
1231
|
}
|
|
958
1232
|
break;
|
|
959
1233
|
}
|
|
960
|
-
|
|
1234
|
+
|
|
961
1235
|
case 'escalation': {
|
|
962
1236
|
const { levels } = step;
|
|
963
1237
|
let finalResult = null;
|
|
964
1238
|
let currentTimeout = 0;
|
|
965
1239
|
let completedLevel = null;
|
|
966
|
-
|
|
967
1240
|
for (const level of levels) {
|
|
968
1241
|
if (level.timeout === 0) {
|
|
969
|
-
// Immediate execution (no timeout)
|
|
970
1242
|
const levelSteps = require('./parser').parseBlock(level.steps);
|
|
971
1243
|
for (const levelStep of levelSteps) {
|
|
972
1244
|
await this.executeStep(levelStep, agentResolver);
|
|
973
1245
|
}
|
|
974
|
-
|
|
975
|
-
// Check if the target variable was set in this level
|
|
976
1246
|
if (levelSteps.length > 0) {
|
|
977
1247
|
const lastStep = levelSteps[levelSteps.length - 1];
|
|
978
1248
|
if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
|
|
@@ -982,13 +1252,10 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
982
1252
|
}
|
|
983
1253
|
}
|
|
984
1254
|
} else {
|
|
985
|
-
// Timed execution for this level
|
|
986
1255
|
currentTimeout += level.timeout;
|
|
987
|
-
|
|
988
1256
|
const timeoutPromise = new Promise(resolve => {
|
|
989
1257
|
setTimeout(() => resolve({ timedOut: true }), level.timeout);
|
|
990
1258
|
});
|
|
991
|
-
|
|
992
1259
|
const levelPromise = (async () => {
|
|
993
1260
|
const levelSteps = require('./parser').parseBlock(level.steps);
|
|
994
1261
|
for (const levelStep of levelSteps) {
|
|
@@ -996,11 +1263,8 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
996
1263
|
}
|
|
997
1264
|
return { timedOut: false };
|
|
998
1265
|
})();
|
|
999
|
-
|
|
1000
1266
|
const result = await Promise.race([timeoutPromise, levelPromise]);
|
|
1001
|
-
|
|
1002
1267
|
if (!result.timedOut) {
|
|
1003
|
-
// Level completed successfully
|
|
1004
1268
|
if (levelSteps && levelSteps.length > 0) {
|
|
1005
1269
|
const lastStep = levelSteps[levelSteps.length - 1];
|
|
1006
1270
|
if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
|
|
@@ -1010,32 +1274,27 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1010
1274
|
}
|
|
1011
1275
|
}
|
|
1012
1276
|
}
|
|
1013
|
-
// If timed out, continue to next level
|
|
1014
1277
|
}
|
|
1015
1278
|
}
|
|
1016
|
-
|
|
1017
|
-
// Set escalation status in context
|
|
1018
1279
|
this.context.escalation_completed = finalResult !== null;
|
|
1019
1280
|
this.context.timed_out = finalResult === null;
|
|
1020
1281
|
if (completedLevel !== null) {
|
|
1021
1282
|
this.context.escalation_level = completedLevel;
|
|
1022
1283
|
}
|
|
1023
|
-
|
|
1024
1284
|
break;
|
|
1025
1285
|
}
|
|
1026
|
-
|
|
1286
|
+
|
|
1027
1287
|
case 'connect': {
|
|
1028
1288
|
this.resources[step.resource] = step.endpoint;
|
|
1029
1289
|
break;
|
|
1030
1290
|
}
|
|
1031
|
-
|
|
1291
|
+
|
|
1032
1292
|
case 'agent_use': {
|
|
1033
1293
|
this.agentMap[step.logicalName] = step.resource;
|
|
1034
1294
|
break;
|
|
1035
1295
|
}
|
|
1036
|
-
|
|
1296
|
+
|
|
1037
1297
|
case 'debrief': {
|
|
1038
|
-
// ✅ SEMANTIC VALIDATION: Check symbols in message
|
|
1039
1298
|
if (step.message.includes('{')) {
|
|
1040
1299
|
const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
|
|
1041
1300
|
for (const symbolMatch of symbols) {
|
|
@@ -1046,22 +1305,17 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1046
1305
|
this.emit('debrief', { agent: step.agent, message: step.message });
|
|
1047
1306
|
break;
|
|
1048
1307
|
}
|
|
1049
|
-
|
|
1050
|
-
// ✅ NEW: Prompt step handler
|
|
1308
|
+
|
|
1051
1309
|
case 'prompt': {
|
|
1052
1310
|
if (this.verbose) {
|
|
1053
1311
|
console.log(`❓ Prompt: ${step.question}`);
|
|
1054
1312
|
}
|
|
1055
|
-
// In non-interactive mode, leave as no-op
|
|
1056
1313
|
break;
|
|
1057
1314
|
}
|
|
1058
|
-
|
|
1059
|
-
// ✅ NEW: Emit step handler with semantic validation
|
|
1315
|
+
|
|
1060
1316
|
case 'emit': {
|
|
1061
|
-
// ✅ SEMANTIC VALIDATION: Check all symbols in payload
|
|
1062
1317
|
const payloadTemplate = step.payload;
|
|
1063
1318
|
const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
|
|
1064
|
-
|
|
1065
1319
|
let shouldEmit = true;
|
|
1066
1320
|
for (const symbolMatch of symbols) {
|
|
1067
1321
|
const symbol = symbolMatch.replace(/[{}]/g, '');
|
|
@@ -1069,78 +1323,62 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1069
1323
|
shouldEmit = false;
|
|
1070
1324
|
}
|
|
1071
1325
|
}
|
|
1072
|
-
|
|
1073
1326
|
if (!shouldEmit) {
|
|
1074
1327
|
if (this.verbose) {
|
|
1075
1328
|
console.log(`⏭️ Skipped emit due to missing semantic symbols`);
|
|
1076
1329
|
}
|
|
1077
1330
|
break;
|
|
1078
1331
|
}
|
|
1079
|
-
|
|
1080
|
-
// ✅ SAFE INTERPOLATION for emit payload
|
|
1081
1332
|
const payload = this._safeInterpolate(step.payload, this.context, 'emit payload');
|
|
1082
|
-
|
|
1083
|
-
this.emit(step.event, {
|
|
1333
|
+
this.emit(step.event, {
|
|
1084
1334
|
payload: payload,
|
|
1085
1335
|
workflow: this.context.workflow_name,
|
|
1086
1336
|
timestamp: new Date().toISOString()
|
|
1087
1337
|
});
|
|
1088
|
-
|
|
1089
1338
|
if (this.verbose) {
|
|
1090
1339
|
console.log(`📤 Emit event "${step.event}" with payload: ${payload}`);
|
|
1091
1340
|
}
|
|
1092
1341
|
break;
|
|
1093
1342
|
}
|
|
1094
|
-
|
|
1095
|
-
// ✅ File Persist step handler with semantic validation
|
|
1343
|
+
|
|
1096
1344
|
case 'persist': {
|
|
1097
|
-
// ✅ SEMANTIC VALIDATION: Require symbol exists
|
|
1098
1345
|
if (!this._requireSemantic(step.variable, 'persist')) {
|
|
1099
1346
|
if (this.verbose) {
|
|
1100
1347
|
console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
|
|
1101
1348
|
}
|
|
1102
1349
|
break;
|
|
1103
1350
|
}
|
|
1104
|
-
|
|
1105
1351
|
const sourceValue = this.context[step.variable];
|
|
1106
1352
|
const outputPath = path.resolve(process.cwd(), step.target);
|
|
1107
1353
|
const outputDir = path.dirname(outputPath);
|
|
1108
1354
|
if (!fs.existsSync(outputDir)) {
|
|
1109
1355
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
1110
1356
|
}
|
|
1111
|
-
|
|
1112
1357
|
let content;
|
|
1113
1358
|
if (step.target.endsWith('.json')) {
|
|
1114
1359
|
content = JSON.stringify(sourceValue, null, 2);
|
|
1115
1360
|
} else {
|
|
1116
1361
|
content = String(sourceValue);
|
|
1117
1362
|
}
|
|
1118
|
-
|
|
1119
1363
|
fs.writeFileSync(outputPath, content, 'utf8');
|
|
1120
|
-
|
|
1121
1364
|
if (this.verbose) {
|
|
1122
1365
|
console.log(`💾 Persisted "${step.variable}" to ${step.target}`);
|
|
1123
1366
|
}
|
|
1124
1367
|
break;
|
|
1125
1368
|
}
|
|
1126
|
-
|
|
1127
|
-
// ✅ NEW: Database persist handler with semantic validation
|
|
1369
|
+
|
|
1128
1370
|
case 'persist-db': {
|
|
1129
1371
|
if (!this.dbClient) {
|
|
1130
1372
|
this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
|
|
1131
1373
|
break;
|
|
1132
1374
|
}
|
|
1133
|
-
|
|
1134
|
-
// ✅ SEMANTIC VALIDATION: Require symbol exists
|
|
1135
1375
|
if (!this._requireSemantic(step.variable, 'persist-db')) {
|
|
1136
1376
|
if (this.verbose) {
|
|
1137
1377
|
console.log(`⏭️ Skipped DB persist for undefined "${step.variable}"`);
|
|
1138
1378
|
}
|
|
1139
1379
|
break;
|
|
1140
1380
|
}
|
|
1141
|
-
|
|
1142
1381
|
const sourceValue = this.context[step.variable];
|
|
1143
|
-
|
|
1144
1382
|
try {
|
|
1145
1383
|
switch (this.dbClient.type) {
|
|
1146
1384
|
case 'postgres':
|
|
@@ -1157,16 +1395,14 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1157
1395
|
);
|
|
1158
1396
|
}
|
|
1159
1397
|
break;
|
|
1160
|
-
|
|
1161
1398
|
case 'mongodb':
|
|
1162
1399
|
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
1163
1400
|
await db.collection(step.collection).insertOne({
|
|
1164
1401
|
workflow_name: this.context.workflow_name || 'unknown',
|
|
1165
|
-
data: sourceValue,
|
|
1402
|
+
data: sourceValue,
|
|
1166
1403
|
created_at: new Date()
|
|
1167
1404
|
});
|
|
1168
1405
|
break;
|
|
1169
|
-
|
|
1170
1406
|
case 'sqlite':
|
|
1171
1407
|
const stmt = this.dbClient.client.prepare(
|
|
1172
1408
|
`INSERT INTO ${step.collection} (workflow_name, data, created_at) VALUES (?, ?, ?)`
|
|
@@ -1178,7 +1414,6 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1178
1414
|
);
|
|
1179
1415
|
break;
|
|
1180
1416
|
}
|
|
1181
|
-
|
|
1182
1417
|
if (this.verbose) {
|
|
1183
1418
|
console.log(`🗄️ Persisted "${step.variable}" to DB collection ${step.collection}`);
|
|
1184
1419
|
}
|
|
@@ -1199,22 +1434,41 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1199
1434
|
if (workflow.type !== 'workflow') {
|
|
1200
1435
|
throw new Error(`Unknown workflow type: ${workflow.type}`);
|
|
1201
1436
|
}
|
|
1202
|
-
|
|
1203
|
-
this.context = {
|
|
1204
|
-
...inputs,
|
|
1205
|
-
workflow_name: workflow.name
|
|
1437
|
+
|
|
1438
|
+
this.context = {
|
|
1439
|
+
...inputs,
|
|
1440
|
+
workflow_name: workflow.name
|
|
1206
1441
|
};
|
|
1207
1442
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1443
|
+
// ✅ AUDIT LOG: Workflow start (ENHANCED with governance metadata)
|
|
1444
|
+
const governanceHash = this._generateGovernanceProfileHash(workflow);
|
|
1445
|
+
|
|
1446
|
+
this._createAuditEntry('workflow_started', {
|
|
1447
|
+
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`, // ✅ Workflow ID
|
|
1448
|
+
workflow_name: workflow.name,
|
|
1449
|
+
workflow_version: workflow.version || null,
|
|
1450
|
+
kernel_version: KERNEL_VERSION, // ✅ Kernel Version
|
|
1451
|
+
runtime_version: KERNEL_VERSION, // ✅ Runtime Version (same as kernel for now)
|
|
1452
|
+
governance_profile_hash: governanceHash, // ✅ Governance Profile Hash
|
|
1453
|
+
inputs_count: Object.keys(inputs).length,
|
|
1454
|
+
steps_count: workflow.steps.length,
|
|
1455
|
+
allowed_resolvers: workflow.allowedResolvers || [],
|
|
1456
|
+
constraints: {
|
|
1457
|
+
max_generations: workflow.maxGenerations,
|
|
1458
|
+
strict_inputs: process.env.OLANG_STRICT_INPUTS === 'true'
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
// Optional strict mode: enforce resolver-originated inputs
|
|
1463
|
+
if (process.env.OLANG_STRICT_INPUTS === 'true') {
|
|
1464
|
+
if (!inputs.__resolver_origin) {
|
|
1465
|
+
throw new Error(
|
|
1466
|
+
'[O-Lang SAFETY] Inputs must originate from a certified resolver. ' +
|
|
1467
|
+
'Use @o-lang/input-validator to validate external data.'
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1215
1470
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1471
|
+
|
|
1218
1472
|
const currentGeneration = inputs.__generation || 1;
|
|
1219
1473
|
if (workflow.maxGenerations !== null && currentGeneration > workflow.maxGenerations) {
|
|
1220
1474
|
throw new Error(`Workflow generation ${currentGeneration} exceeds Constraint: max_generations = ${workflow.maxGenerations}`);
|
|
@@ -1222,10 +1476,8 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1222
1476
|
|
|
1223
1477
|
this.workflowSteps = workflow.steps;
|
|
1224
1478
|
this.allowedResolvers = new Set(workflow.allowedResolvers || []);
|
|
1225
|
-
|
|
1226
1479
|
const mathPattern =
|
|
1227
1480
|
/^(Add|Subtract|Multiply|Divide|Sum|Avg|Min|Max|Round|Floor|Ceil|Abs)\b/i;
|
|
1228
|
-
|
|
1229
1481
|
for (const step of workflow.steps) {
|
|
1230
1482
|
if (step.type === 'calculate' || (step.actionRaw && mathPattern.test(step.actionRaw))) {
|
|
1231
1483
|
this.allowedResolvers.add('builtInMathResolver');
|
|
@@ -1235,9 +1487,23 @@ const isLLMAction = action.toLowerCase().includes('groq') ||
|
|
|
1235
1487
|
for (const step of workflow.steps) {
|
|
1236
1488
|
await this.executeStep(step, agentResolver);
|
|
1237
1489
|
}
|
|
1490
|
+
|
|
1491
|
+
// ✅ AUDIT LOG: Workflow completion (ENHANCED)
|
|
1492
|
+
this._createAuditEntry('workflow_completed', {
|
|
1493
|
+
workflow_id: `${workflow.name}@${workflow.version || 'unversioned'}`,
|
|
1494
|
+
workflow_name: workflow.name,
|
|
1495
|
+
kernel_version: KERNEL_VERSION,
|
|
1496
|
+
runtime_version: KERNEL_VERSION,
|
|
1497
|
+
governance_profile_hash: governanceHash,
|
|
1498
|
+
return_values: workflow.returnValues,
|
|
1499
|
+
total_steps: workflow.steps.length,
|
|
1500
|
+
execution_summary: {
|
|
1501
|
+
warnings: this.__warnings.length,
|
|
1502
|
+
disallowed_attempts: this.disallowedAttempts.length
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1238
1505
|
|
|
1239
1506
|
this.printDisallowedSummary();
|
|
1240
|
-
|
|
1241
1507
|
if (this.__warnings.length) {
|
|
1242
1508
|
console.log(`\n[O-Lang] ⚠️ Parser/Runtime Warnings (${this.__warnings.length}):`);
|
|
1243
1509
|
this.__warnings.slice(0, 5).forEach((w, i) => {
|