@edictum/core 0.1.0 → 0.2.0
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/README.md +46 -0
- package/dist/{chunk-IXMXZGJG.mjs → chunk-23XIQZR5.mjs} +1 -1
- package/dist/chunk-23XIQZR5.mjs.map +1 -0
- package/dist/{chunk-CRPQFRYJ.mjs → chunk-2YSBMUK5.mjs} +2 -10
- package/dist/chunk-2YSBMUK5.mjs.map +1 -0
- package/dist/{chunk-X5E2YY35.mjs → chunk-JOBPRXVE.mjs} +44 -110
- package/dist/chunk-JOBPRXVE.mjs.map +1 -0
- package/dist/{dry-run-54PYIM6T.mjs → dry-run-JTRNTZA5.mjs} +3 -3
- package/dist/dry-run-JTRNTZA5.mjs.map +1 -0
- package/dist/index.cjs +413 -224
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -27
- package/dist/index.d.ts +28 -27
- package/dist/index.mjs +370 -118
- package/dist/index.mjs.map +1 -1
- package/dist/runner-JCAQMF6O.mjs +10 -0
- package/package.json +14 -13
- package/dist/chunk-CRPQFRYJ.mjs.map +0 -1
- package/dist/chunk-IXMXZGJG.mjs.map +0 -1
- package/dist/chunk-X5E2YY35.mjs.map +0 -1
- package/dist/dry-run-54PYIM6T.mjs.map +0 -1
- package/dist/runner-ASI4JIW2.mjs +0 -10
- /package/dist/{runner-ASI4JIW2.mjs.map → runner-JCAQMF6O.mjs.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -81,7 +81,7 @@ function _validateToolName(toolName) {
|
|
|
81
81
|
for (let i = 0; i < toolName.length; i++) {
|
|
82
82
|
const code = toolName.charCodeAt(i);
|
|
83
83
|
const ch = toolName[i];
|
|
84
|
-
if (code < 32 || code
|
|
84
|
+
if (code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233 || ch === "/" || ch === "\\") {
|
|
85
85
|
throw new EdictumConfigError(`Invalid tool_name: ${JSON.stringify(toolName)}`);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -323,7 +323,7 @@ function _validateStorageKeyComponent(value, label) {
|
|
|
323
323
|
}
|
|
324
324
|
for (let i = 0; i < value.length; i++) {
|
|
325
325
|
const code = value.charCodeAt(i);
|
|
326
|
-
if (code < 32 || code ===
|
|
326
|
+
if (code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233) {
|
|
327
327
|
throw new EdictumConfigError(`Invalid ${label}: ${JSON.stringify(value)}`);
|
|
328
328
|
}
|
|
329
329
|
}
|
|
@@ -368,14 +368,10 @@ var init_session = __esm({
|
|
|
368
368
|
}
|
|
369
369
|
async toolExecutionCount(tool) {
|
|
370
370
|
_validateStorageKeyComponent(tool, "tool_name");
|
|
371
|
-
return Number(
|
|
372
|
-
await this._backend.get(`s:${this._sid}:tool:${tool}`) ?? 0
|
|
373
|
-
);
|
|
371
|
+
return Number(await this._backend.get(`s:${this._sid}:tool:${tool}`) ?? 0);
|
|
374
372
|
}
|
|
375
373
|
async consecutiveFailures() {
|
|
376
|
-
return Number(
|
|
377
|
-
await this._backend.get(`s:${this._sid}:consec_fail`) ?? 0
|
|
378
|
-
);
|
|
374
|
+
return Number(await this._backend.get(`s:${this._sid}:consec_fail`) ?? 0);
|
|
379
375
|
}
|
|
380
376
|
/**
|
|
381
377
|
* Pre-fetch multiple session counters in a single backend call.
|
|
@@ -388,10 +384,7 @@ var init_session = __esm({
|
|
|
388
384
|
* backends without batchGet support.
|
|
389
385
|
*/
|
|
390
386
|
async batchGetCounters(options) {
|
|
391
|
-
const keys = [
|
|
392
|
-
`s:${this._sid}:attempts`,
|
|
393
|
-
`s:${this._sid}:execs`
|
|
394
|
-
];
|
|
387
|
+
const keys = [`s:${this._sid}:attempts`, `s:${this._sid}:execs`];
|
|
395
388
|
const keyLabels = ["attempts", "execs"];
|
|
396
389
|
if (options?.includeTool != null) {
|
|
397
390
|
_validateStorageKeyComponent(options.includeTool, "tool_name");
|
|
@@ -448,11 +441,8 @@ var init_redaction = __esm({
|
|
|
448
441
|
"passphrase"
|
|
449
442
|
]);
|
|
450
443
|
static BASH_REDACTION_PATTERNS = [
|
|
451
|
-
[
|
|
452
|
-
|
|
453
|
-
"$1[REDACTED]"
|
|
454
|
-
],
|
|
455
|
-
[String.raw`(-p\s*|--password[= ])\S+`, "$1[REDACTED]"],
|
|
444
|
+
[String.raw`(export\s+\w*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)\w*=)\S+`, "$1[REDACTED]"],
|
|
445
|
+
[String.raw`((?:^|\s)-p\s*|--password[= ])\S+`, "$1[REDACTED]"],
|
|
456
446
|
[String.raw`(://\w+:)\S+(@)`, "$1[REDACTED]$2"]
|
|
457
447
|
];
|
|
458
448
|
static SECRET_VALUE_PATTERNS = [
|
|
@@ -482,25 +472,18 @@ var init_redaction = __esm({
|
|
|
482
472
|
}
|
|
483
473
|
}
|
|
484
474
|
}
|
|
485
|
-
this._patterns = [
|
|
486
|
-
...customPatterns ?? [],
|
|
487
|
-
..._RedactionPolicy.BASH_REDACTION_PATTERNS
|
|
488
|
-
];
|
|
475
|
+
this._patterns = [...customPatterns ?? [], ..._RedactionPolicy.BASH_REDACTION_PATTERNS];
|
|
489
476
|
this._compiledPatterns = this._patterns.map(
|
|
490
477
|
([pattern, replacement]) => [new RegExp(pattern, "g"), replacement]
|
|
491
478
|
);
|
|
492
|
-
this._compiledSecretPatterns = _RedactionPolicy.SECRET_VALUE_PATTERNS.map(
|
|
493
|
-
(p) => new RegExp(p)
|
|
494
|
-
);
|
|
479
|
+
this._compiledSecretPatterns = _RedactionPolicy.SECRET_VALUE_PATTERNS.map((p) => new RegExp(p));
|
|
495
480
|
this._detectValues = detectSecretValues;
|
|
496
481
|
}
|
|
497
482
|
/** Recursively redact sensitive data from tool arguments. */
|
|
498
483
|
redactArgs(args) {
|
|
499
484
|
if (args !== null && typeof args === "object" && !Array.isArray(args)) {
|
|
500
485
|
const result = {};
|
|
501
|
-
for (const [key, value] of Object.entries(
|
|
502
|
-
args
|
|
503
|
-
)) {
|
|
486
|
+
for (const [key, value] of Object.entries(args)) {
|
|
504
487
|
result[key] = this._isSensitiveKey(key) ? "[REDACTED]" : this.redactArgs(value);
|
|
505
488
|
}
|
|
506
489
|
return result;
|
|
@@ -512,6 +495,18 @@ var init_redaction = __esm({
|
|
|
512
495
|
if (this._detectValues && this._looksLikeSecret(args)) {
|
|
513
496
|
return "[REDACTED]";
|
|
514
497
|
}
|
|
498
|
+
if (this._detectValues) {
|
|
499
|
+
const capped = args.length > _RedactionPolicy.MAX_REGEX_INPUT ? args.slice(0, _RedactionPolicy.MAX_REGEX_INPUT) : args;
|
|
500
|
+
let redacted = capped;
|
|
501
|
+
for (const [pattern, replacement] of this._compiledPatterns) {
|
|
502
|
+
pattern.lastIndex = 0;
|
|
503
|
+
redacted = redacted.replace(pattern, replacement);
|
|
504
|
+
}
|
|
505
|
+
if (redacted.length > 1e3) {
|
|
506
|
+
return redacted.slice(0, 997) + "...";
|
|
507
|
+
}
|
|
508
|
+
return redacted;
|
|
509
|
+
}
|
|
515
510
|
if (args.length > 1e3) {
|
|
516
511
|
return args.slice(0, 997) + "...";
|
|
517
512
|
}
|
|
@@ -584,7 +579,7 @@ var init_redaction = __esm({
|
|
|
584
579
|
|
|
585
580
|
// src/approval.ts
|
|
586
581
|
function sanitizeForTerminal(s) {
|
|
587
|
-
return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/[\x00-\x1f\x7f]/g, "");
|
|
582
|
+
return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, "");
|
|
588
583
|
}
|
|
589
584
|
function createApprovalRequest(fields) {
|
|
590
585
|
const request = {
|
|
@@ -655,9 +650,7 @@ var init_approval = __esm({
|
|
|
655
650
|
const effectiveTimeout = timeout ?? (request ? request.timeout : 300);
|
|
656
651
|
try {
|
|
657
652
|
const response = await this._readStdin(approvalId, effectiveTimeout);
|
|
658
|
-
const approved = ["y", "yes", "approve"].includes(
|
|
659
|
-
response.trim().toLowerCase()
|
|
660
|
-
);
|
|
653
|
+
const approved = ["y", "yes", "approve"].includes(response.trim().toLowerCase());
|
|
661
654
|
const status = approved ? ApprovalStatus.APPROVED : ApprovalStatus.DENIED;
|
|
662
655
|
return createApprovalDecision({
|
|
663
656
|
approved,
|
|
@@ -1139,10 +1132,7 @@ var init_pipeline = __esm({
|
|
|
1139
1132
|
}
|
|
1140
1133
|
}
|
|
1141
1134
|
const pe = hasPolicyError(contractsEvaluated);
|
|
1142
|
-
const observeResults = await this._evaluateObserveContracts(
|
|
1143
|
-
envelope,
|
|
1144
|
-
session
|
|
1145
|
-
);
|
|
1135
|
+
const observeResults = await this._evaluateObserveContracts(envelope, session);
|
|
1146
1136
|
return createPreDecision({
|
|
1147
1137
|
action: "allow",
|
|
1148
1138
|
hooksEvaluated,
|
|
@@ -1197,23 +1187,17 @@ var init_pipeline = __esm({
|
|
|
1197
1187
|
text = policy.redactResult(text, text.length + 100);
|
|
1198
1188
|
}
|
|
1199
1189
|
redactedResponse = text;
|
|
1200
|
-
warnings.push(
|
|
1201
|
-
`\u26A0\uFE0F Content redacted by ${contract.name}.`
|
|
1202
|
-
);
|
|
1190
|
+
warnings.push(`\u26A0\uFE0F Content redacted by ${contract.name}.`);
|
|
1203
1191
|
} else if (effect === "deny" && isSafe) {
|
|
1204
1192
|
redactedResponse = `[OUTPUT SUPPRESSED] ${verdict.message}`;
|
|
1205
1193
|
outputSuppressed = true;
|
|
1206
|
-
warnings.push(
|
|
1207
|
-
`\u26A0\uFE0F Output suppressed by ${contract.name}.`
|
|
1208
|
-
);
|
|
1194
|
+
warnings.push(`\u26A0\uFE0F Output suppressed by ${contract.name}.`);
|
|
1209
1195
|
} else if ((effect === "redact" || effect === "deny") && !isSafe) {
|
|
1210
1196
|
warnings.push(
|
|
1211
1197
|
`\u26A0\uFE0F ${verdict.message} Tool already executed \u2014 assess before proceeding.`
|
|
1212
1198
|
);
|
|
1213
1199
|
} else if (isSafe) {
|
|
1214
|
-
warnings.push(
|
|
1215
|
-
`\u26A0\uFE0F ${verdict.message} Consider retrying.`
|
|
1216
|
-
);
|
|
1200
|
+
warnings.push(`\u26A0\uFE0F ${verdict.message} Consider retrying.`);
|
|
1217
1201
|
} else {
|
|
1218
1202
|
warnings.push(
|
|
1219
1203
|
`\u26A0\uFE0F ${verdict.message} Tool already executed \u2014 assess before proceeding.`
|
|
@@ -1235,10 +1219,7 @@ var init_pipeline = __esm({
|
|
|
1235
1219
|
try {
|
|
1236
1220
|
verdict = await contract.check(envelope, toolResponse);
|
|
1237
1221
|
} catch (exc) {
|
|
1238
|
-
verdict = Verdict.fail(
|
|
1239
|
-
`Observe-mode postcondition error: ${exc}`,
|
|
1240
|
-
{ policy_error: true }
|
|
1241
|
-
);
|
|
1222
|
+
verdict = Verdict.fail(`Observe-mode postcondition error: ${exc}`, { policy_error: true });
|
|
1242
1223
|
}
|
|
1243
1224
|
const record = {
|
|
1244
1225
|
name: contract.name,
|
|
@@ -1256,9 +1237,7 @@ var init_pipeline = __esm({
|
|
|
1256
1237
|
warnings.push(`\u26A0\uFE0F [observe] ${verdict.message}`);
|
|
1257
1238
|
}
|
|
1258
1239
|
}
|
|
1259
|
-
const postconditionsPassed = contractsEvaluated.length > 0 ? contractsEvaluated.every(
|
|
1260
|
-
(c) => c["passed"] === true || c["observed"] === true
|
|
1261
|
-
) : true;
|
|
1240
|
+
const postconditionsPassed = contractsEvaluated.length > 0 ? contractsEvaluated.every((c) => c["passed"] === true || c["observed"] === true) : true;
|
|
1262
1241
|
const pe = hasPolicyError(contractsEvaluated);
|
|
1263
1242
|
return createPostDecision({
|
|
1264
1243
|
toolSuccess,
|
|
@@ -1279,17 +1258,12 @@ var init_pipeline = __esm({
|
|
|
1279
1258
|
*/
|
|
1280
1259
|
async _evaluateObserveContracts(envelope, session) {
|
|
1281
1260
|
const results = [];
|
|
1282
|
-
for (const contract of this._guard.getObservePreconditions(
|
|
1283
|
-
envelope
|
|
1284
|
-
)) {
|
|
1261
|
+
for (const contract of this._guard.getObservePreconditions(envelope)) {
|
|
1285
1262
|
let verdict;
|
|
1286
1263
|
try {
|
|
1287
1264
|
verdict = await contract.check(envelope);
|
|
1288
1265
|
} catch (exc) {
|
|
1289
|
-
verdict = Verdict.fail(
|
|
1290
|
-
`Observe-mode precondition error: ${exc}`,
|
|
1291
|
-
{ policy_error: true }
|
|
1292
|
-
);
|
|
1266
|
+
verdict = Verdict.fail(`Observe-mode precondition error: ${exc}`, { policy_error: true });
|
|
1293
1267
|
}
|
|
1294
1268
|
results.push({
|
|
1295
1269
|
name: contract.name,
|
|
@@ -1299,17 +1273,12 @@ var init_pipeline = __esm({
|
|
|
1299
1273
|
source: contract.source ?? "yaml_precondition"
|
|
1300
1274
|
});
|
|
1301
1275
|
}
|
|
1302
|
-
for (const contract of this._guard.getObserveSandboxContracts(
|
|
1303
|
-
envelope
|
|
1304
|
-
)) {
|
|
1276
|
+
for (const contract of this._guard.getObserveSandboxContracts(envelope)) {
|
|
1305
1277
|
let verdict;
|
|
1306
1278
|
try {
|
|
1307
1279
|
verdict = await contract.check(envelope);
|
|
1308
1280
|
} catch (exc) {
|
|
1309
|
-
verdict = Verdict.fail(
|
|
1310
|
-
`Observe-mode sandbox error: ${exc}`,
|
|
1311
|
-
{ policy_error: true }
|
|
1312
|
-
);
|
|
1281
|
+
verdict = Verdict.fail(`Observe-mode sandbox error: ${exc}`, { policy_error: true });
|
|
1313
1282
|
}
|
|
1314
1283
|
results.push({
|
|
1315
1284
|
name: contract.name,
|
|
@@ -1324,10 +1293,9 @@ var init_pipeline = __esm({
|
|
|
1324
1293
|
try {
|
|
1325
1294
|
verdict = await contract.check(session);
|
|
1326
1295
|
} catch (exc) {
|
|
1327
|
-
verdict = Verdict.fail(
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
);
|
|
1296
|
+
verdict = Verdict.fail(`Observe-mode session contract error: ${exc}`, {
|
|
1297
|
+
policy_error: true
|
|
1298
|
+
});
|
|
1331
1299
|
}
|
|
1332
1300
|
results.push({
|
|
1333
1301
|
name: contract.name,
|
|
@@ -1430,46 +1398,22 @@ async function run(guard, toolName, args, toolCallable, options) {
|
|
|
1430
1398
|
principal: principalDict
|
|
1431
1399
|
}
|
|
1432
1400
|
);
|
|
1433
|
-
await _emitRunPreAudit(
|
|
1434
|
-
guard,
|
|
1435
|
-
envelope,
|
|
1436
|
-
session,
|
|
1437
|
-
AuditAction.CALL_APPROVAL_REQUESTED,
|
|
1438
|
-
pre
|
|
1439
|
-
);
|
|
1401
|
+
await _emitRunPreAudit(guard, envelope, session, AuditAction.CALL_APPROVAL_REQUESTED, pre);
|
|
1440
1402
|
const decision = await guard._approvalBackend.waitForDecision(
|
|
1441
1403
|
approvalRequest.approvalId,
|
|
1442
1404
|
pre.approvalTimeout
|
|
1443
1405
|
);
|
|
1444
1406
|
let approved = false;
|
|
1445
1407
|
if (decision.status === ApprovalStatus.TIMEOUT) {
|
|
1446
|
-
await _emitRunPreAudit(
|
|
1447
|
-
guard,
|
|
1448
|
-
envelope,
|
|
1449
|
-
session,
|
|
1450
|
-
AuditAction.CALL_APPROVAL_TIMEOUT,
|
|
1451
|
-
pre
|
|
1452
|
-
);
|
|
1408
|
+
await _emitRunPreAudit(guard, envelope, session, AuditAction.CALL_APPROVAL_TIMEOUT, pre);
|
|
1453
1409
|
if (pre.approvalTimeoutEffect === "allow") {
|
|
1454
1410
|
approved = true;
|
|
1455
1411
|
}
|
|
1456
1412
|
} else if (!decision.approved) {
|
|
1457
|
-
await _emitRunPreAudit(
|
|
1458
|
-
guard,
|
|
1459
|
-
envelope,
|
|
1460
|
-
session,
|
|
1461
|
-
AuditAction.CALL_APPROVAL_DENIED,
|
|
1462
|
-
pre
|
|
1463
|
-
);
|
|
1413
|
+
await _emitRunPreAudit(guard, envelope, session, AuditAction.CALL_APPROVAL_DENIED, pre);
|
|
1464
1414
|
} else {
|
|
1465
1415
|
approved = true;
|
|
1466
|
-
await _emitRunPreAudit(
|
|
1467
|
-
guard,
|
|
1468
|
-
envelope,
|
|
1469
|
-
session,
|
|
1470
|
-
AuditAction.CALL_APPROVAL_GRANTED,
|
|
1471
|
-
pre
|
|
1472
|
-
);
|
|
1416
|
+
await _emitRunPreAudit(guard, envelope, session, AuditAction.CALL_APPROVAL_GRANTED, pre);
|
|
1473
1417
|
}
|
|
1474
1418
|
if (approved) {
|
|
1475
1419
|
if (guard._onAllow) {
|
|
@@ -1505,11 +1449,7 @@ async function run(guard, toolName, args, toolCallable, options) {
|
|
|
1505
1449
|
} catch {
|
|
1506
1450
|
}
|
|
1507
1451
|
}
|
|
1508
|
-
throw new EdictumDenied(
|
|
1509
|
-
pre.reason ?? "denied",
|
|
1510
|
-
pre.decisionSource,
|
|
1511
|
-
pre.decisionName
|
|
1512
|
-
);
|
|
1452
|
+
throw new EdictumDenied(pre.reason ?? "denied", pre.decisionSource, pre.decisionName);
|
|
1513
1453
|
}
|
|
1514
1454
|
} else {
|
|
1515
1455
|
for (const cr of pre.contractsEvaluated) {
|
|
@@ -1533,13 +1473,7 @@ async function run(guard, toolName, args, toolCallable, options) {
|
|
|
1533
1473
|
await guard.auditSink.emit(observedEvent);
|
|
1534
1474
|
}
|
|
1535
1475
|
}
|
|
1536
|
-
await _emitRunPreAudit(
|
|
1537
|
-
guard,
|
|
1538
|
-
envelope,
|
|
1539
|
-
session,
|
|
1540
|
-
AuditAction.CALL_ALLOWED,
|
|
1541
|
-
pre
|
|
1542
|
-
);
|
|
1476
|
+
await _emitRunPreAudit(guard, envelope, session, AuditAction.CALL_ALLOWED, pre);
|
|
1543
1477
|
if (guard._onAllow) {
|
|
1544
1478
|
try {
|
|
1545
1479
|
guard._onAllow(envelope);
|
|
@@ -1966,21 +1900,15 @@ function classifyFinding(contractId, verdictMessage) {
|
|
|
1966
1900
|
const contractLower = contractId.toLowerCase();
|
|
1967
1901
|
const messageLower = (verdictMessage || "").toLowerCase();
|
|
1968
1902
|
const piiTerms = ["pii", "ssn", "patient", "name", "dob"];
|
|
1969
|
-
if (piiTerms.some(
|
|
1970
|
-
(term) => contractLower.includes(term) || messageLower.includes(term)
|
|
1971
|
-
)) {
|
|
1903
|
+
if (piiTerms.some((term) => contractLower.includes(term) || messageLower.includes(term))) {
|
|
1972
1904
|
return "pii_detected";
|
|
1973
1905
|
}
|
|
1974
1906
|
const secretTerms = ["secret", "token", "key", "credential", "password"];
|
|
1975
|
-
if (secretTerms.some(
|
|
1976
|
-
(term) => contractLower.includes(term) || messageLower.includes(term)
|
|
1977
|
-
)) {
|
|
1907
|
+
if (secretTerms.some((term) => contractLower.includes(term) || messageLower.includes(term))) {
|
|
1978
1908
|
return "secret_detected";
|
|
1979
1909
|
}
|
|
1980
1910
|
const limitTerms = ["session", "limit", "max_calls", "budget"];
|
|
1981
|
-
if (limitTerms.some(
|
|
1982
|
-
(term) => contractLower.includes(term) || messageLower.includes(term)
|
|
1983
|
-
)) {
|
|
1911
|
+
if (limitTerms.some((term) => contractLower.includes(term) || messageLower.includes(term))) {
|
|
1984
1912
|
return "limit_exceeded";
|
|
1985
1913
|
}
|
|
1986
1914
|
return "policy_violation";
|
|
@@ -2127,9 +2055,7 @@ function mergeObserveAlongside(merged, layer, label, contractSources, observes)
|
|
|
2127
2055
|
for (const contract of layer.contracts ?? []) {
|
|
2128
2056
|
const cid = contract.id;
|
|
2129
2057
|
const observeId = `${cid}:candidate`;
|
|
2130
|
-
const existingIds = new Set(
|
|
2131
|
-
mc.map((c) => c.id)
|
|
2132
|
-
);
|
|
2058
|
+
const existingIds = new Set(mc.map((c) => c.id));
|
|
2133
2059
|
if (existingIds.has(observeId)) {
|
|
2134
2060
|
throw new EdictumConfigError(
|
|
2135
2061
|
`observe_alongside collision: generated ID "${observeId}" already exists in the bundle. Rename the conflicting contract or use a different ID for "${cid}".`
|
|
@@ -2276,10 +2202,7 @@ function resolveSelector(selector, envelope, outputText, customSelectors) {
|
|
|
2276
2202
|
if (rest === "role") return envelope.principal.role;
|
|
2277
2203
|
if (rest === "ticket_ref") return envelope.principal.ticketRef;
|
|
2278
2204
|
if (rest.startsWith("claims.")) {
|
|
2279
|
-
return resolveNested(
|
|
2280
|
-
rest.slice(7),
|
|
2281
|
-
envelope.principal.claims
|
|
2282
|
-
);
|
|
2205
|
+
return resolveNested(rest.slice(7), envelope.principal.claims);
|
|
2283
2206
|
}
|
|
2284
2207
|
return _MISSING;
|
|
2285
2208
|
}
|
|
@@ -2293,10 +2216,7 @@ function resolveSelector(selector, envelope, outputText, customSelectors) {
|
|
|
2293
2216
|
return coerceEnvValue(raw);
|
|
2294
2217
|
}
|
|
2295
2218
|
if (selector.startsWith("metadata.")) {
|
|
2296
|
-
return resolveNested(
|
|
2297
|
-
selector.slice(9),
|
|
2298
|
-
envelope.metadata
|
|
2299
|
-
);
|
|
2219
|
+
return resolveNested(selector.slice(9), envelope.metadata);
|
|
2300
2220
|
}
|
|
2301
2221
|
if (customSelectors) {
|
|
2302
2222
|
const dotPos = selector.indexOf(".");
|
|
@@ -2345,13 +2265,31 @@ function evaluateExpression(expr, envelope, outputText, options) {
|
|
|
2345
2265
|
const customOps = options?.customOperators ?? null;
|
|
2346
2266
|
const customSels = options?.customSelectors ?? null;
|
|
2347
2267
|
if ("all" in expr) {
|
|
2348
|
-
return _evalAll(
|
|
2268
|
+
return _evalAll(
|
|
2269
|
+
expr.all,
|
|
2270
|
+
envelope,
|
|
2271
|
+
outputText,
|
|
2272
|
+
customOps,
|
|
2273
|
+
customSels
|
|
2274
|
+
);
|
|
2349
2275
|
}
|
|
2350
2276
|
if ("any" in expr) {
|
|
2351
|
-
return _evalAny(
|
|
2277
|
+
return _evalAny(
|
|
2278
|
+
expr.any,
|
|
2279
|
+
envelope,
|
|
2280
|
+
outputText,
|
|
2281
|
+
customOps,
|
|
2282
|
+
customSels
|
|
2283
|
+
);
|
|
2352
2284
|
}
|
|
2353
2285
|
if ("not" in expr) {
|
|
2354
|
-
return _evalNot(
|
|
2286
|
+
return _evalNot(
|
|
2287
|
+
expr.not,
|
|
2288
|
+
envelope,
|
|
2289
|
+
outputText,
|
|
2290
|
+
customOps,
|
|
2291
|
+
customSels
|
|
2292
|
+
);
|
|
2355
2293
|
}
|
|
2356
2294
|
const leafKeys = Object.keys(expr);
|
|
2357
2295
|
if (leafKeys.length !== 1) {
|
|
@@ -2406,7 +2344,8 @@ function _applyOperator(op, fieldValue, opValue, selector, customOperators) {
|
|
|
2406
2344
|
}
|
|
2407
2345
|
if (fieldValue === _MISSING || fieldValue == null) return false;
|
|
2408
2346
|
try {
|
|
2409
|
-
if (Object.hasOwn(OPERATORS, op))
|
|
2347
|
+
if (Object.hasOwn(OPERATORS, op))
|
|
2348
|
+
return OPERATORS[op](fieldValue, opValue);
|
|
2410
2349
|
if (customOperators && Object.hasOwn(customOperators, op)) {
|
|
2411
2350
|
return Boolean(customOperators[op](fieldValue, opValue));
|
|
2412
2351
|
}
|
|
@@ -2433,10 +2372,7 @@ function expandMessage(template, envelope, outputText, customSelectors) {
|
|
|
2433
2372
|
});
|
|
2434
2373
|
}
|
|
2435
2374
|
function validateOperators(bundle, customOperators) {
|
|
2436
|
-
const known = /* @__PURE__ */ new Set([
|
|
2437
|
-
...BUILTIN_OPERATOR_NAMES,
|
|
2438
|
-
...Object.keys(customOperators ?? {})
|
|
2439
|
-
]);
|
|
2375
|
+
const known = /* @__PURE__ */ new Set([...BUILTIN_OPERATOR_NAMES, ...Object.keys(customOperators ?? {})]);
|
|
2440
2376
|
const contracts = bundle.contracts ?? [];
|
|
2441
2377
|
for (const contract of contracts) {
|
|
2442
2378
|
const when = contract.when;
|
|
@@ -2468,9 +2404,7 @@ function _validateExpressionOperators(expr, known, contractId) {
|
|
|
2468
2404
|
if (operator != null && typeof operator === "object") {
|
|
2469
2405
|
for (const opName of Object.keys(operator)) {
|
|
2470
2406
|
if (!known.has(opName)) {
|
|
2471
|
-
throw new EdictumConfigError(
|
|
2472
|
-
`Contract '${contractId}': unknown operator '${opName}'`
|
|
2473
|
-
);
|
|
2407
|
+
throw new EdictumConfigError(`Contract '${contractId}': unknown operator '${opName}'`);
|
|
2474
2408
|
}
|
|
2475
2409
|
}
|
|
2476
2410
|
}
|
|
@@ -2607,7 +2541,16 @@ function compilePost(contract, mode, customOps, customSels) {
|
|
|
2607
2541
|
const meta = then.metadata ?? {};
|
|
2608
2542
|
const check = (envelope, response) => {
|
|
2609
2543
|
const outputText = response != null ? String(response) : void 0;
|
|
2610
|
-
return _evalAndVerdict(
|
|
2544
|
+
return _evalAndVerdict(
|
|
2545
|
+
whenExpr,
|
|
2546
|
+
envelope,
|
|
2547
|
+
outputText,
|
|
2548
|
+
msgTpl,
|
|
2549
|
+
tags,
|
|
2550
|
+
meta,
|
|
2551
|
+
customOps,
|
|
2552
|
+
customSels
|
|
2553
|
+
);
|
|
2611
2554
|
};
|
|
2612
2555
|
const effectValue = then.effect ?? "warn";
|
|
2613
2556
|
const result = {
|
|
@@ -2672,14 +2615,18 @@ function mergeSessionLimits(contract, existing) {
|
|
|
2672
2615
|
if ("max_tool_calls" in sessionLimits) {
|
|
2673
2616
|
const raw = sessionLimits.max_tool_calls;
|
|
2674
2617
|
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
2675
|
-
throw new EdictumConfigError(
|
|
2618
|
+
throw new EdictumConfigError(
|
|
2619
|
+
`Session limit max_tool_calls must be a finite number, got: ${String(raw)}`
|
|
2620
|
+
);
|
|
2676
2621
|
}
|
|
2677
2622
|
maxToolCalls = Math.min(maxToolCalls, raw);
|
|
2678
2623
|
}
|
|
2679
2624
|
if ("max_attempts" in sessionLimits) {
|
|
2680
2625
|
const raw = sessionLimits.max_attempts;
|
|
2681
2626
|
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
2682
|
-
throw new EdictumConfigError(
|
|
2627
|
+
throw new EdictumConfigError(
|
|
2628
|
+
`Session limit max_attempts must be a finite number, got: ${String(raw)}`
|
|
2629
|
+
);
|
|
2683
2630
|
}
|
|
2684
2631
|
maxAttempts = Math.min(maxAttempts, raw);
|
|
2685
2632
|
}
|
|
@@ -2704,10 +2651,6 @@ function mergeSessionLimits(contract, existing) {
|
|
|
2704
2651
|
// src/yaml-engine/sandbox-compile-fn.ts
|
|
2705
2652
|
init_contracts();
|
|
2706
2653
|
|
|
2707
|
-
// src/yaml-engine/sandbox-compiler.ts
|
|
2708
|
-
var import_node_fs = require("fs");
|
|
2709
|
-
var import_node_path = require("path");
|
|
2710
|
-
|
|
2711
2654
|
// src/fnmatch.ts
|
|
2712
2655
|
function fnmatch(name, pattern) {
|
|
2713
2656
|
if (pattern === "*") return true;
|
|
@@ -2732,6 +2675,36 @@ function fnmatch(name, pattern) {
|
|
|
2732
2675
|
return new RegExp("^" + regex + "$").test(safeName);
|
|
2733
2676
|
}
|
|
2734
2677
|
|
|
2678
|
+
// src/yaml-engine/resolve-path.ts
|
|
2679
|
+
var import_node_fs = require("fs");
|
|
2680
|
+
var import_node_path = require("path");
|
|
2681
|
+
function resolvePath(p) {
|
|
2682
|
+
const cleaned = p.replace(/\0/g, "");
|
|
2683
|
+
const resolved = (0, import_node_path.resolve)(cleaned);
|
|
2684
|
+
try {
|
|
2685
|
+
return (0, import_node_fs.realpathSync)(resolved);
|
|
2686
|
+
} catch (err) {
|
|
2687
|
+
if (err.code !== "ENOENT") {
|
|
2688
|
+
return resolved;
|
|
2689
|
+
}
|
|
2690
|
+
const parts = resolved.split(import_node_path.sep);
|
|
2691
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
2692
|
+
const prefix = parts.slice(0, i).join(import_node_path.sep) || "/";
|
|
2693
|
+
try {
|
|
2694
|
+
const realPrefix = (0, import_node_fs.realpathSync)(prefix);
|
|
2695
|
+
const rest = parts.slice(i).join(import_node_path.sep);
|
|
2696
|
+
return (0, import_node_path.join)(realPrefix, rest);
|
|
2697
|
+
} catch (innerErr) {
|
|
2698
|
+
if (innerErr.code !== "ENOENT") {
|
|
2699
|
+
return resolved;
|
|
2700
|
+
}
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return resolved;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2735
2708
|
// src/yaml-engine/sandbox-compiler.ts
|
|
2736
2709
|
var _REDIRECT_PREFIX_RE = /^(?:\d*>>|>>|\d*>|>|<<|<)/;
|
|
2737
2710
|
var _SHELL_SEPARATOR_RE = /[;|&\n\r`]|\$\(|\$\{|<\(/;
|
|
@@ -2807,21 +2780,25 @@ var _PATH_ARG_KEYS = /* @__PURE__ */ new Set([
|
|
|
2807
2780
|
"destination",
|
|
2808
2781
|
"source",
|
|
2809
2782
|
"src",
|
|
2810
|
-
"dst"
|
|
2783
|
+
"dst",
|
|
2784
|
+
// Common path-like arg names from AI framework tool calls.
|
|
2785
|
+
// IMPORTANT: Only include keys that are unambiguously path-related.
|
|
2786
|
+
// Generic keys like 'name', 'input', 'output', 'from', 'to' are excluded
|
|
2787
|
+
// because they frequently hold non-path values (e.g., { from: "English" }),
|
|
2788
|
+
// and resolvePath() would produce false positives. The heuristic loop below
|
|
2789
|
+
// catches path-like VALUES in any key (containing '..', '~', or '/').
|
|
2790
|
+
"filename",
|
|
2791
|
+
"file",
|
|
2792
|
+
"filepath",
|
|
2793
|
+
"read_path",
|
|
2794
|
+
"write_path"
|
|
2811
2795
|
]);
|
|
2812
|
-
function _realpath(p) {
|
|
2813
|
-
try {
|
|
2814
|
-
return (0, import_node_fs.realpathSync)(p);
|
|
2815
|
-
} catch {
|
|
2816
|
-
return (0, import_node_path.resolve)(p);
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
2796
|
function extractPaths(envelope) {
|
|
2820
2797
|
const paths = [];
|
|
2821
2798
|
const seen = /* @__PURE__ */ new Set();
|
|
2822
2799
|
function add(p) {
|
|
2823
2800
|
if (!p) return;
|
|
2824
|
-
const resolved =
|
|
2801
|
+
const resolved = resolvePath(p);
|
|
2825
2802
|
if (!seen.has(resolved)) {
|
|
2826
2803
|
seen.add(resolved);
|
|
2827
2804
|
paths.push(resolved);
|
|
@@ -2835,6 +2812,17 @@ function extractPaths(envelope) {
|
|
|
2835
2812
|
for (const [key, value] of Object.entries(args)) {
|
|
2836
2813
|
if (typeof value === "string" && value.startsWith("/") && !_PATH_ARG_KEYS.has(key)) add(value);
|
|
2837
2814
|
}
|
|
2815
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2816
|
+
if (typeof value === "string" && !_PATH_ARG_KEYS.has(key)) {
|
|
2817
|
+
const isUrl = value.includes("://");
|
|
2818
|
+
if (!isUrl && value.includes("../") || // embedded traversal: foo/../etc/passwd
|
|
2819
|
+
value === ".." || // bare parent reference
|
|
2820
|
+
value.startsWith("../") || // parent traversal prefix: ../../etc/passwd
|
|
2821
|
+
value.startsWith("~") || value.startsWith("./") && !isUrl) {
|
|
2822
|
+
add(value);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2838
2826
|
const cmd = envelope.bashCommand ?? args.command ?? "";
|
|
2839
2827
|
if (cmd) {
|
|
2840
2828
|
for (const token of tokenizeCommand(cmd)) {
|
|
@@ -2887,23 +2875,14 @@ function domainMatches(hostname, patterns) {
|
|
|
2887
2875
|
}
|
|
2888
2876
|
|
|
2889
2877
|
// src/yaml-engine/sandbox-compile-fn.ts
|
|
2890
|
-
var import_node_fs2 = require("fs");
|
|
2891
|
-
var import_node_path2 = require("path");
|
|
2892
|
-
function _realpath2(p) {
|
|
2893
|
-
try {
|
|
2894
|
-
return (0, import_node_fs2.realpathSync)(p);
|
|
2895
|
-
} catch {
|
|
2896
|
-
return (0, import_node_path2.resolve)(p);
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
2878
|
function _pathWithin(filePath, prefix) {
|
|
2900
2879
|
return filePath === prefix || filePath.startsWith(prefix.replace(/\/+$/, "") + "/");
|
|
2901
2880
|
}
|
|
2902
2881
|
function compileSandbox(contract, mode) {
|
|
2903
2882
|
const contractId = contract.id;
|
|
2904
2883
|
const toolPatterns = "tools" in contract ? contract.tools : [contract.tool];
|
|
2905
|
-
const within = (contract.within ?? []).map(
|
|
2906
|
-
const notWithin = (contract.not_within ?? []).map(
|
|
2884
|
+
const within = (contract.within ?? []).map(resolvePath);
|
|
2885
|
+
const notWithin = (contract.not_within ?? []).map(resolvePath);
|
|
2907
2886
|
const allows = contract.allows ?? {};
|
|
2908
2887
|
const notAllows = contract.not_allows ?? {};
|
|
2909
2888
|
const allowedCommands = allows.commands ?? [];
|
|
@@ -2916,6 +2895,11 @@ function compileSandbox(contract, mode) {
|
|
|
2916
2895
|
const check = (envelope) => {
|
|
2917
2896
|
if (within.length > 0 || notWithin.length > 0) {
|
|
2918
2897
|
const paths = extractPaths(envelope);
|
|
2898
|
+
if (paths.length === 0 && within.length > 0) {
|
|
2899
|
+
return Verdict.fail(
|
|
2900
|
+
expandMessage(messageTemplate, envelope) + " (no extractable paths \u2014 sandbox cannot verify boundary compliance)"
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2919
2903
|
if (paths.length > 0) {
|
|
2920
2904
|
for (const p of paths) {
|
|
2921
2905
|
for (const excluded of notWithin) {
|
|
@@ -2982,9 +2966,7 @@ function compileContracts(bundle, options = {}) {
|
|
|
2982
2966
|
const customSels = options?.customSelectors ?? null;
|
|
2983
2967
|
validateOperators(bundle, customOps);
|
|
2984
2968
|
if (bundle.defaults == null || typeof bundle.defaults !== "object") {
|
|
2985
|
-
throw new EdictumConfigError(
|
|
2986
|
-
"Bundle missing required 'defaults' section with 'mode' field"
|
|
2987
|
-
);
|
|
2969
|
+
throw new EdictumConfigError("Bundle missing required 'defaults' section with 'mode' field");
|
|
2988
2970
|
}
|
|
2989
2971
|
const defaults = bundle.defaults;
|
|
2990
2972
|
const defaultMode = defaults.mode;
|
|
@@ -3030,7 +3012,8 @@ function compileContracts(bundle, options = {}) {
|
|
|
3030
3012
|
|
|
3031
3013
|
// src/yaml-engine/loader.ts
|
|
3032
3014
|
var import_node_crypto3 = require("crypto");
|
|
3033
|
-
var
|
|
3015
|
+
var import_node_fs2 = require("fs");
|
|
3016
|
+
var import_js_yaml = __toESM(require("js-yaml"), 1);
|
|
3034
3017
|
init_errors();
|
|
3035
3018
|
|
|
3036
3019
|
// src/yaml-engine/loader-validators.ts
|
|
@@ -3053,13 +3036,22 @@ function validateSchema(data) {
|
|
|
3053
3036
|
throw new EdictumConfigError("Schema validation failed: contracts must be an array");
|
|
3054
3037
|
}
|
|
3055
3038
|
}
|
|
3056
|
-
var CONTROL_CHAR_RE = /[\x00-\x1f\x7f-\x9f]/;
|
|
3039
|
+
var CONTROL_CHAR_RE = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/;
|
|
3040
|
+
var CONTRACT_ID_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
3057
3041
|
function validateContractId(contractId) {
|
|
3042
|
+
if (contractId.length > 1e4) {
|
|
3043
|
+
throw new EdictumConfigError("Contract id exceeds maximum length");
|
|
3044
|
+
}
|
|
3058
3045
|
if (CONTROL_CHAR_RE.test(contractId)) {
|
|
3059
3046
|
throw new EdictumConfigError(
|
|
3060
3047
|
`Contract id contains control characters: '${contractId.replace(CONTROL_CHAR_RE, "\\x??")}'`
|
|
3061
3048
|
);
|
|
3062
3049
|
}
|
|
3050
|
+
if (!CONTRACT_ID_RE.test(contractId)) {
|
|
3051
|
+
throw new EdictumConfigError(
|
|
3052
|
+
`Contract id '${contractId}' must match pattern ^[a-z0-9][a-z0-9_-]*$`
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3063
3055
|
}
|
|
3064
3056
|
function validateUniqueIds(data) {
|
|
3065
3057
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -3160,26 +3152,231 @@ function validateSandboxContracts(data) {
|
|
|
3160
3152
|
}
|
|
3161
3153
|
}
|
|
3162
3154
|
|
|
3155
|
+
// src/yaml-engine/loader-field-validators.ts
|
|
3156
|
+
init_errors();
|
|
3157
|
+
var VALID_CONTRACT_TYPES = /* @__PURE__ */ new Set(["pre", "post", "session", "sandbox"]);
|
|
3158
|
+
var PRE_EFFECTS = /* @__PURE__ */ new Set(["deny", "approve"]);
|
|
3159
|
+
var POST_EFFECTS = /* @__PURE__ */ new Set(["warn", "redact", "deny"]);
|
|
3160
|
+
var VALID_MODES = /* @__PURE__ */ new Set(["enforce", "observe"]);
|
|
3161
|
+
var VALID_SIDE_EFFECTS = /* @__PURE__ */ new Set(["pure", "read", "write", "irreversible"]);
|
|
3162
|
+
var KNOWN_TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
3163
|
+
"apiVersion",
|
|
3164
|
+
"kind",
|
|
3165
|
+
"metadata",
|
|
3166
|
+
"defaults",
|
|
3167
|
+
"contracts",
|
|
3168
|
+
"tools",
|
|
3169
|
+
"observability",
|
|
3170
|
+
"observe_alongside"
|
|
3171
|
+
]);
|
|
3172
|
+
var METADATA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
3173
|
+
var MAX_MESSAGE_LENGTH = 500;
|
|
3174
|
+
function fail(msg) {
|
|
3175
|
+
throw new EdictumConfigError(`Schema validation failed: ${msg}`);
|
|
3176
|
+
}
|
|
3177
|
+
function validateContractFields(data) {
|
|
3178
|
+
for (const key of Object.keys(data)) {
|
|
3179
|
+
if (!KNOWN_TOP_LEVEL.has(key)) fail(`unknown top-level field '${key}'`);
|
|
3180
|
+
}
|
|
3181
|
+
if (data.metadata == null || typeof data.metadata !== "object" || Array.isArray(data.metadata)) {
|
|
3182
|
+
fail("'metadata' is required and must be an object");
|
|
3183
|
+
}
|
|
3184
|
+
const meta = data.metadata;
|
|
3185
|
+
if (meta.name == null || typeof meta.name !== "string") fail("metadata.name is required");
|
|
3186
|
+
const metaName = meta.name;
|
|
3187
|
+
if (metaName.length > 1e4) fail("metadata.name exceeds maximum length");
|
|
3188
|
+
if (!METADATA_NAME_RE.test(metaName)) {
|
|
3189
|
+
fail(
|
|
3190
|
+
`metadata.name must be a lowercase slug (^[a-z0-9][a-z0-9._-]*$), got '${metaName.slice(0, 100)}'`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
if (data.defaults == null || typeof data.defaults !== "object" || Array.isArray(data.defaults)) {
|
|
3194
|
+
fail("'defaults' is required and must be an object");
|
|
3195
|
+
}
|
|
3196
|
+
const mode = data.defaults.mode;
|
|
3197
|
+
if (!VALID_MODES.has(mode)) {
|
|
3198
|
+
fail(`defaults.mode must be 'enforce' or 'observe', got '${String(mode)}'`);
|
|
3199
|
+
}
|
|
3200
|
+
const contracts = data.contracts;
|
|
3201
|
+
if (contracts.length === 0) fail("contracts must contain at least 1 item");
|
|
3202
|
+
if (data.tools != null) {
|
|
3203
|
+
if (typeof data.tools !== "object" || Array.isArray(data.tools)) {
|
|
3204
|
+
fail("'tools' must be a mapping of tool names to descriptors, not an array");
|
|
3205
|
+
}
|
|
3206
|
+
for (const [tn, td] of Object.entries(data.tools)) {
|
|
3207
|
+
if (td == null || typeof td !== "object" || Array.isArray(td)) {
|
|
3208
|
+
fail(`tools.${tn} must be an object with a 'side_effect' field`);
|
|
3209
|
+
}
|
|
3210
|
+
const se = td.side_effect;
|
|
3211
|
+
if (!VALID_SIDE_EFFECTS.has(se)) {
|
|
3212
|
+
fail(
|
|
3213
|
+
`tools.${tn}.side_effect must be 'pure', 'read', 'write', or 'irreversible', got '${String(se)}'`
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
for (const c of contracts) {
|
|
3219
|
+
if (c == null || typeof c !== "object" || Array.isArray(c)) {
|
|
3220
|
+
fail("every contract must be an object (got null or non-object array element)");
|
|
3221
|
+
}
|
|
3222
|
+
if (c.id == null || typeof c.id !== "string" || c.id.length === 0) {
|
|
3223
|
+
fail("every contract requires a non-empty 'id' string");
|
|
3224
|
+
}
|
|
3225
|
+
const cid = c.id;
|
|
3226
|
+
if (!VALID_CONTRACT_TYPES.has(c.type)) {
|
|
3227
|
+
fail(`contract '${cid}': invalid type '${String(c.type)}'`);
|
|
3228
|
+
}
|
|
3229
|
+
const t = c.type;
|
|
3230
|
+
if (t === "pre" || t === "post") validatePrePost(c, t, cid);
|
|
3231
|
+
else if (t === "session") validateSession(c, cid);
|
|
3232
|
+
else if (t === "sandbox") validateSandboxStructure(c, cid);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
function validatePrePost(c, t, cid) {
|
|
3236
|
+
if (c.tool == null || typeof c.tool !== "string") {
|
|
3237
|
+
fail(`${t} contract '${cid}' requires 'tool' to be a string`);
|
|
3238
|
+
}
|
|
3239
|
+
if (c.when == null || typeof c.when !== "object" || Array.isArray(c.when)) {
|
|
3240
|
+
fail(
|
|
3241
|
+
`${t} contract '${cid}' requires 'when' to be a mapping (got ${Array.isArray(c.when) ? "array" : typeof c.when})`
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
if (c.then == null || typeof c.then !== "object" || Array.isArray(c.then)) {
|
|
3245
|
+
fail(`${t} contract '${cid}' requires 'then' to be a mapping`);
|
|
3246
|
+
}
|
|
3247
|
+
const then = c.then;
|
|
3248
|
+
if (then.effect == null) fail(`${t} contract '${cid}' requires 'then.effect'`);
|
|
3249
|
+
if (then.message == null) fail(`${t} contract '${cid}' requires 'then.message'`);
|
|
3250
|
+
validateMessageLength(then.message, `${t} contract '${cid}'`);
|
|
3251
|
+
const effect = then.effect;
|
|
3252
|
+
if (t === "pre" && !PRE_EFFECTS.has(effect)) {
|
|
3253
|
+
fail(`pre contract '${cid}': effect must be 'deny' or 'approve', got '${effect}'`);
|
|
3254
|
+
}
|
|
3255
|
+
if (t === "post" && !POST_EFFECTS.has(effect)) {
|
|
3256
|
+
fail(`post contract '${cid}': effect must be 'warn', 'redact', or 'deny', got '${effect}'`);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
function validateSession(c, cid) {
|
|
3260
|
+
if (c.limits == null || typeof c.limits !== "object" || Array.isArray(c.limits)) {
|
|
3261
|
+
fail(`session contract '${cid}' requires 'limits' to be a mapping`);
|
|
3262
|
+
}
|
|
3263
|
+
const lim = c.limits;
|
|
3264
|
+
if (!("max_tool_calls" in lim) && !("max_attempts" in lim) && !("max_calls_per_tool" in lim)) {
|
|
3265
|
+
fail(
|
|
3266
|
+
`session contract '${cid}': limits must have max_tool_calls, max_attempts, or max_calls_per_tool`
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
if (c.then == null || typeof c.then !== "object" || Array.isArray(c.then)) {
|
|
3270
|
+
fail(`session contract '${cid}' requires 'then' to be a mapping`);
|
|
3271
|
+
}
|
|
3272
|
+
const then = c.then;
|
|
3273
|
+
if (then.effect !== "deny") {
|
|
3274
|
+
fail(`session contract '${cid}': effect must be 'deny', got '${String(then.effect)}'`);
|
|
3275
|
+
}
|
|
3276
|
+
if (then.message == null) fail(`session contract '${cid}' requires 'then.message'`);
|
|
3277
|
+
validateMessageLength(then.message, `session contract '${cid}'`);
|
|
3278
|
+
}
|
|
3279
|
+
function validateSandboxStructure(c, cid) {
|
|
3280
|
+
if (c.tool == null && c.tools == null) {
|
|
3281
|
+
fail(`sandbox contract '${cid}' requires either 'tool' or 'tools'`);
|
|
3282
|
+
}
|
|
3283
|
+
if (c.tool != null && typeof c.tool !== "string") {
|
|
3284
|
+
fail(`sandbox contract '${cid}': 'tool' must be a string`);
|
|
3285
|
+
}
|
|
3286
|
+
if (c.tools != null && (!Array.isArray(c.tools) || c.tools.length === 0)) {
|
|
3287
|
+
fail(`sandbox contract '${cid}': 'tools' must be a non-empty array`);
|
|
3288
|
+
}
|
|
3289
|
+
if (c.within == null && c.allows == null) {
|
|
3290
|
+
fail(`sandbox contract '${cid}' requires either 'within' or 'allows'`);
|
|
3291
|
+
}
|
|
3292
|
+
if (c.within != null && (!Array.isArray(c.within) || c.within.length === 0)) {
|
|
3293
|
+
fail(`sandbox contract '${cid}': 'within' must be a non-empty array`);
|
|
3294
|
+
}
|
|
3295
|
+
if (c.message == null) fail(`sandbox contract '${cid}' requires 'message'`);
|
|
3296
|
+
validateMessageLength(c.message, `sandbox contract '${cid}'`);
|
|
3297
|
+
}
|
|
3298
|
+
function validateMessageLength(msg, context) {
|
|
3299
|
+
if (typeof msg !== "string") {
|
|
3300
|
+
fail(`${context}: message must be a string`);
|
|
3301
|
+
}
|
|
3302
|
+
if (msg.length > MAX_MESSAGE_LENGTH) {
|
|
3303
|
+
fail(`${context}: message exceeds ${MAX_MESSAGE_LENGTH} characters`);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
// src/yaml-engine/loader-expression-validators.ts
|
|
3308
|
+
init_errors();
|
|
3309
|
+
var NUMERIC_OPS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
|
|
3310
|
+
var STRING_OPS = /* @__PURE__ */ new Set(["contains", "starts_with", "ends_with"]);
|
|
3311
|
+
var ARRAY_MIN1_OPS = /* @__PURE__ */ new Set(["in", "not_in", "contains_any", "matches_any"]);
|
|
3312
|
+
function fail2(msg) {
|
|
3313
|
+
throw new EdictumConfigError(`Schema validation failed: ${msg}`);
|
|
3314
|
+
}
|
|
3315
|
+
function validateExpressionShapes(data) {
|
|
3316
|
+
for (const c of data.contracts ?? []) {
|
|
3317
|
+
if (c == null || typeof c !== "object") continue;
|
|
3318
|
+
const contract = c;
|
|
3319
|
+
if (contract.when != null) checkExprShape(contract.when, contract.id ?? "?");
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
var MAX_EXPR_DEPTH = 50;
|
|
3323
|
+
function checkExprShape(expr, cid, depth = 0) {
|
|
3324
|
+
if (depth > MAX_EXPR_DEPTH)
|
|
3325
|
+
fail2(`contract '${cid}': expression nesting exceeds maximum depth (${MAX_EXPR_DEPTH})`);
|
|
3326
|
+
if (expr == null || typeof expr !== "object") return;
|
|
3327
|
+
const e = expr;
|
|
3328
|
+
if ("all" in e) {
|
|
3329
|
+
const a = e.all;
|
|
3330
|
+
if (!Array.isArray(a) || a.length === 0)
|
|
3331
|
+
fail2(`contract '${cid}': 'all' requires a non-empty array`);
|
|
3332
|
+
for (const s of a) checkExprShape(s, cid, depth + 1);
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
if ("any" in e) {
|
|
3336
|
+
const a = e.any;
|
|
3337
|
+
if (!Array.isArray(a) || a.length === 0)
|
|
3338
|
+
fail2(`contract '${cid}': 'any' requires a non-empty array`);
|
|
3339
|
+
for (const s of a) checkExprShape(s, cid, depth + 1);
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
if ("not" in e) {
|
|
3343
|
+
if (e.not == null || typeof e.not !== "object" || Array.isArray(e.not)) {
|
|
3344
|
+
fail2(
|
|
3345
|
+
`contract '${cid}': 'not' requires an expression mapping (got ${e.not === null ? "null" : Array.isArray(e.not) ? "array" : typeof e.not})`
|
|
3346
|
+
);
|
|
3347
|
+
}
|
|
3348
|
+
checkExprShape(e.not, cid, depth + 1);
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
for (const v of Object.values(e)) {
|
|
3352
|
+
if (v == null || typeof v !== "object") continue;
|
|
3353
|
+
if (Array.isArray(v)) {
|
|
3354
|
+
fail2(`contract '${cid}': selector value must be an operator mapping, not an array`);
|
|
3355
|
+
}
|
|
3356
|
+
const op = v;
|
|
3357
|
+
for (const [name, val] of Object.entries(op)) {
|
|
3358
|
+
if (NUMERIC_OPS.has(name) && typeof val !== "number") {
|
|
3359
|
+
fail2(`contract '${cid}': operator '${name}' requires a number, got ${typeof val}`);
|
|
3360
|
+
}
|
|
3361
|
+
if (STRING_OPS.has(name) && typeof val !== "string") {
|
|
3362
|
+
fail2(`contract '${cid}': operator '${name}' requires a string, got ${typeof val}`);
|
|
3363
|
+
}
|
|
3364
|
+
if (ARRAY_MIN1_OPS.has(name) && (!Array.isArray(val) || val.length === 0)) {
|
|
3365
|
+
fail2(`contract '${cid}': operator '${name}' requires a non-empty array`);
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3163
3371
|
// src/yaml-engine/loader.ts
|
|
3164
3372
|
var MAX_BUNDLE_SIZE = 1048576;
|
|
3165
3373
|
function computeHash(rawBytes) {
|
|
3166
3374
|
return { hex: (0, import_node_crypto3.createHash)("sha256").update(rawBytes).digest("hex") };
|
|
3167
3375
|
}
|
|
3168
|
-
function requireYaml() {
|
|
3169
|
-
try {
|
|
3170
|
-
const yaml = require("js-yaml");
|
|
3171
|
-
return yaml;
|
|
3172
|
-
} catch {
|
|
3173
|
-
throw new EdictumConfigError(
|
|
3174
|
-
"The YAML engine requires js-yaml. Install it with: npm install js-yaml"
|
|
3175
|
-
);
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
3376
|
function parseYaml(content) {
|
|
3179
|
-
const yaml = requireYaml();
|
|
3180
3377
|
let data;
|
|
3181
3378
|
try {
|
|
3182
|
-
data =
|
|
3379
|
+
data = import_js_yaml.default.load(content, { schema: import_js_yaml.default.CORE_SCHEMA });
|
|
3183
3380
|
} catch (e) {
|
|
3184
3381
|
throw new EdictumConfigError(`YAML parse error: ${String(e)}`);
|
|
3185
3382
|
}
|
|
@@ -3190,20 +3387,22 @@ function parseYaml(content) {
|
|
|
3190
3387
|
}
|
|
3191
3388
|
function validateBundle(data) {
|
|
3192
3389
|
validateSchema(data);
|
|
3390
|
+
validateContractFields(data);
|
|
3193
3391
|
validateUniqueIds(data);
|
|
3392
|
+
validateExpressionShapes(data);
|
|
3194
3393
|
validateRegexes(data);
|
|
3195
3394
|
validatePreSelectors(data);
|
|
3196
3395
|
validateSandboxContracts(data);
|
|
3197
3396
|
}
|
|
3198
3397
|
function loadBundle(source) {
|
|
3199
|
-
const resolved = (0,
|
|
3200
|
-
const fileSize = (0,
|
|
3398
|
+
const resolved = (0, import_node_fs2.realpathSync)(source);
|
|
3399
|
+
const fileSize = (0, import_node_fs2.statSync)(resolved).size;
|
|
3201
3400
|
if (fileSize > MAX_BUNDLE_SIZE) {
|
|
3202
3401
|
throw new EdictumConfigError(
|
|
3203
3402
|
`Bundle file too large (${fileSize} bytes, max ${MAX_BUNDLE_SIZE})`
|
|
3204
3403
|
);
|
|
3205
3404
|
}
|
|
3206
|
-
const rawBytes = (0,
|
|
3405
|
+
const rawBytes = (0, import_node_fs2.readFileSync)(resolved);
|
|
3207
3406
|
const bundleHash = computeHash(rawBytes);
|
|
3208
3407
|
const data = parseYaml(rawBytes.toString("utf-8"));
|
|
3209
3408
|
validateBundle(data);
|
|
@@ -3251,9 +3450,10 @@ function fromYaml(...args) {
|
|
|
3251
3450
|
policyVersion = entry[1].hex;
|
|
3252
3451
|
report = { overriddenContracts: [], observeContracts: [] };
|
|
3253
3452
|
} else {
|
|
3254
|
-
const bundleTuples = loaded.map(
|
|
3255
|
-
|
|
3256
|
-
|
|
3453
|
+
const bundleTuples = loaded.map(([data], i) => [
|
|
3454
|
+
data,
|
|
3455
|
+
paths[i]
|
|
3456
|
+
]);
|
|
3257
3457
|
const composed = composeBundles(...bundleTuples);
|
|
3258
3458
|
bundleData = composed.bundle;
|
|
3259
3459
|
report = composed.report;
|
|
@@ -3378,15 +3578,9 @@ var Edictum = class _Edictum {
|
|
|
3378
3578
|
this._approvalBackend = options.approvalBackend ?? null;
|
|
3379
3579
|
this._localSink = new CollectingAuditSink();
|
|
3380
3580
|
if (Array.isArray(options.auditSink)) {
|
|
3381
|
-
this.auditSink = new CompositeSink([
|
|
3382
|
-
this._localSink,
|
|
3383
|
-
...options.auditSink
|
|
3384
|
-
]);
|
|
3581
|
+
this.auditSink = new CompositeSink([this._localSink, ...options.auditSink]);
|
|
3385
3582
|
} else if (options.auditSink != null) {
|
|
3386
|
-
this.auditSink = new CompositeSink([
|
|
3387
|
-
this._localSink,
|
|
3388
|
-
options.auditSink
|
|
3389
|
-
]);
|
|
3583
|
+
this.auditSink = new CompositeSink([this._localSink, options.auditSink]);
|
|
3390
3584
|
} else {
|
|
3391
3585
|
this.auditSink = this._localSink;
|
|
3392
3586
|
}
|
|
@@ -3484,24 +3678,16 @@ var Edictum = class _Edictum {
|
|
|
3484
3678
|
}
|
|
3485
3679
|
getHooks(phase, envelope) {
|
|
3486
3680
|
const hooks = phase === "before" ? this._beforeHooks : this._afterHooks;
|
|
3487
|
-
return hooks.filter(
|
|
3488
|
-
(h) => h.tool === "*" || fnmatch(envelope.toolName, h.tool)
|
|
3489
|
-
);
|
|
3681
|
+
return hooks.filter((h) => h.tool === "*" || fnmatch(envelope.toolName, h.tool));
|
|
3490
3682
|
}
|
|
3491
3683
|
// -----------------------------------------------------------------------
|
|
3492
3684
|
// Contract accessors -- enforce mode
|
|
3493
3685
|
// -----------------------------------------------------------------------
|
|
3494
3686
|
getPreconditions(envelope) {
|
|
3495
|
-
return _Edictum._filterByTool(
|
|
3496
|
-
this._state.preconditions,
|
|
3497
|
-
envelope
|
|
3498
|
-
);
|
|
3687
|
+
return _Edictum._filterByTool(this._state.preconditions, envelope);
|
|
3499
3688
|
}
|
|
3500
3689
|
getPostconditions(envelope) {
|
|
3501
|
-
return _Edictum._filterByTool(
|
|
3502
|
-
this._state.postconditions,
|
|
3503
|
-
envelope
|
|
3504
|
-
);
|
|
3690
|
+
return _Edictum._filterByTool(this._state.postconditions, envelope);
|
|
3505
3691
|
}
|
|
3506
3692
|
getSessionContracts() {
|
|
3507
3693
|
return [...this._state.sessionContracts];
|
|
@@ -3561,12 +3747,16 @@ var Edictum = class _Edictum {
|
|
|
3561
3747
|
const edictumType = raw._edictum_type;
|
|
3562
3748
|
const isObserve = raw._edictum_observe ?? raw._edictum_shadow ?? false;
|
|
3563
3749
|
if (edictumType != null) {
|
|
3564
|
-
_Edictum._classifyInternal(
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3750
|
+
_Edictum._classifyInternal(raw, edictumType, isObserve, {
|
|
3751
|
+
pre,
|
|
3752
|
+
post,
|
|
3753
|
+
session,
|
|
3754
|
+
sandbox,
|
|
3755
|
+
oPre,
|
|
3756
|
+
oPost,
|
|
3757
|
+
oSession,
|
|
3758
|
+
oSandbox
|
|
3759
|
+
});
|
|
3570
3760
|
} else if (isSessionContract(item)) {
|
|
3571
3761
|
const name = raw.name ?? "anonymous";
|
|
3572
3762
|
session.push({
|
|
@@ -3624,9 +3814,12 @@ var Edictum = class _Edictum {
|
|
|
3624
3814
|
static _classifyInternal(raw, edictumType, isObserve, lists) {
|
|
3625
3815
|
const target = isObserve ? { pre: lists.oPre, post: lists.oPost, session: lists.oSession, sandbox: lists.oSandbox } : { pre: lists.pre, post: lists.post, session: lists.session, sandbox: lists.sandbox };
|
|
3626
3816
|
if (edictumType === "precondition") target.pre.push(raw);
|
|
3627
|
-
else if (edictumType === "postcondition")
|
|
3628
|
-
|
|
3629
|
-
else if (edictumType === "
|
|
3817
|
+
else if (edictumType === "postcondition")
|
|
3818
|
+
target.post.push(raw);
|
|
3819
|
+
else if (edictumType === "session_contract")
|
|
3820
|
+
target.session.push(raw);
|
|
3821
|
+
else if (edictumType === "sandbox")
|
|
3822
|
+
target.sandbox.push(raw);
|
|
3630
3823
|
else {
|
|
3631
3824
|
throw new EdictumConfigError(
|
|
3632
3825
|
`Unknown _edictum_type "${edictumType}". Expected "precondition", "postcondition", "session_contract", or "sandbox".`
|
|
@@ -3678,15 +3871,11 @@ var Edictum = class _Edictum {
|
|
|
3678
3871
|
* Session contracts are skipped.
|
|
3679
3872
|
*/
|
|
3680
3873
|
evaluate(toolName, args, options) {
|
|
3681
|
-
return Promise.resolve().then(() => (init_dry_run(), dry_run_exports)).then(
|
|
3682
|
-
({ evaluate: evaluate2 }) => evaluate2(this, toolName, args, options)
|
|
3683
|
-
);
|
|
3874
|
+
return Promise.resolve().then(() => (init_dry_run(), dry_run_exports)).then(({ evaluate: evaluate2 }) => evaluate2(this, toolName, args, options));
|
|
3684
3875
|
}
|
|
3685
3876
|
/** Evaluate a batch of tool calls. Thin wrapper over evaluate(). */
|
|
3686
3877
|
evaluateBatch(calls) {
|
|
3687
|
-
return Promise.resolve().then(() => (init_dry_run(), dry_run_exports)).then(
|
|
3688
|
-
({ evaluateBatch: evaluateBatch2 }) => evaluateBatch2(this, calls)
|
|
3689
|
-
);
|
|
3878
|
+
return Promise.resolve().then(() => (init_dry_run(), dry_run_exports)).then(({ evaluateBatch: evaluateBatch2 }) => evaluateBatch2(this, calls));
|
|
3690
3879
|
}
|
|
3691
3880
|
static fromYaml(...args) {
|
|
3692
3881
|
return fromYaml(...args);
|