@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/dist/index.mjs CHANGED
@@ -18,11 +18,11 @@ import {
18
18
  createPreDecision,
19
19
  defaultSuccessCheck,
20
20
  run
21
- } from "./chunk-X5E2YY35.mjs";
21
+ } from "./chunk-JOBPRXVE.mjs";
22
22
  import {
23
23
  createContractResult,
24
24
  createEvaluationResult
25
- } from "./chunk-IXMXZGJG.mjs";
25
+ } from "./chunk-23XIQZR5.mjs";
26
26
  import {
27
27
  BashClassifier,
28
28
  EdictumConfigError,
@@ -30,12 +30,11 @@ import {
30
30
  EdictumToolError,
31
31
  SideEffect,
32
32
  ToolRegistry,
33
- __require,
34
33
  _validateToolName,
35
34
  createEnvelope,
36
35
  createPrincipal,
37
36
  deepFreeze
38
- } from "./chunk-CRPQFRYJ.mjs";
37
+ } from "./chunk-2YSBMUK5.mjs";
39
38
 
40
39
  // src/limits.ts
41
40
  var DEFAULT_LIMITS = Object.freeze({
@@ -108,21 +107,15 @@ function classifyFinding(contractId, verdictMessage) {
108
107
  const contractLower = contractId.toLowerCase();
109
108
  const messageLower = (verdictMessage || "").toLowerCase();
110
109
  const piiTerms = ["pii", "ssn", "patient", "name", "dob"];
111
- if (piiTerms.some(
112
- (term) => contractLower.includes(term) || messageLower.includes(term)
113
- )) {
110
+ if (piiTerms.some((term) => contractLower.includes(term) || messageLower.includes(term))) {
114
111
  return "pii_detected";
115
112
  }
116
113
  const secretTerms = ["secret", "token", "key", "credential", "password"];
117
- if (secretTerms.some(
118
- (term) => contractLower.includes(term) || messageLower.includes(term)
119
- )) {
114
+ if (secretTerms.some((term) => contractLower.includes(term) || messageLower.includes(term))) {
120
115
  return "secret_detected";
121
116
  }
122
117
  const limitTerms = ["session", "limit", "max_calls", "budget"];
123
- if (limitTerms.some(
124
- (term) => contractLower.includes(term) || messageLower.includes(term)
125
- )) {
118
+ if (limitTerms.some((term) => contractLower.includes(term) || messageLower.includes(term))) {
126
119
  return "limit_exceeded";
127
120
  }
128
121
  return "policy_violation";
@@ -263,9 +256,7 @@ function mergeObserveAlongside(merged, layer, label, contractSources, observes)
263
256
  for (const contract of layer.contracts ?? []) {
264
257
  const cid = contract.id;
265
258
  const observeId = `${cid}:candidate`;
266
- const existingIds = new Set(
267
- mc.map((c) => c.id)
268
- );
259
+ const existingIds = new Set(mc.map((c) => c.id));
269
260
  if (existingIds.has(observeId)) {
270
261
  throw new EdictumConfigError(
271
262
  `observe_alongside collision: generated ID "${observeId}" already exists in the bundle. Rename the conflicting contract or use a different ID for "${cid}".`
@@ -405,10 +396,7 @@ function resolveSelector(selector, envelope, outputText, customSelectors) {
405
396
  if (rest === "role") return envelope.principal.role;
406
397
  if (rest === "ticket_ref") return envelope.principal.ticketRef;
407
398
  if (rest.startsWith("claims.")) {
408
- return resolveNested(
409
- rest.slice(7),
410
- envelope.principal.claims
411
- );
399
+ return resolveNested(rest.slice(7), envelope.principal.claims);
412
400
  }
413
401
  return _MISSING;
414
402
  }
@@ -422,10 +410,7 @@ function resolveSelector(selector, envelope, outputText, customSelectors) {
422
410
  return coerceEnvValue(raw);
423
411
  }
424
412
  if (selector.startsWith("metadata.")) {
425
- return resolveNested(
426
- selector.slice(9),
427
- envelope.metadata
428
- );
413
+ return resolveNested(selector.slice(9), envelope.metadata);
429
414
  }
430
415
  if (customSelectors) {
431
416
  const dotPos = selector.indexOf(".");
@@ -474,13 +459,31 @@ function evaluateExpression(expr, envelope, outputText, options) {
474
459
  const customOps = options?.customOperators ?? null;
475
460
  const customSels = options?.customSelectors ?? null;
476
461
  if ("all" in expr) {
477
- return _evalAll(expr.all, envelope, outputText, customOps, customSels);
462
+ return _evalAll(
463
+ expr.all,
464
+ envelope,
465
+ outputText,
466
+ customOps,
467
+ customSels
468
+ );
478
469
  }
479
470
  if ("any" in expr) {
480
- return _evalAny(expr.any, envelope, outputText, customOps, customSels);
471
+ return _evalAny(
472
+ expr.any,
473
+ envelope,
474
+ outputText,
475
+ customOps,
476
+ customSels
477
+ );
481
478
  }
482
479
  if ("not" in expr) {
483
- return _evalNot(expr.not, envelope, outputText, customOps, customSels);
480
+ return _evalNot(
481
+ expr.not,
482
+ envelope,
483
+ outputText,
484
+ customOps,
485
+ customSels
486
+ );
484
487
  }
485
488
  const leafKeys = Object.keys(expr);
486
489
  if (leafKeys.length !== 1) {
@@ -535,7 +538,8 @@ function _applyOperator(op, fieldValue, opValue, selector, customOperators) {
535
538
  }
536
539
  if (fieldValue === _MISSING || fieldValue == null) return false;
537
540
  try {
538
- if (Object.hasOwn(OPERATORS, op)) return OPERATORS[op](fieldValue, opValue);
541
+ if (Object.hasOwn(OPERATORS, op))
542
+ return OPERATORS[op](fieldValue, opValue);
539
543
  if (customOperators && Object.hasOwn(customOperators, op)) {
540
544
  return Boolean(customOperators[op](fieldValue, opValue));
541
545
  }
@@ -562,10 +566,7 @@ function expandMessage(template, envelope, outputText, customSelectors) {
562
566
  });
563
567
  }
564
568
  function validateOperators(bundle, customOperators) {
565
- const known = /* @__PURE__ */ new Set([
566
- ...BUILTIN_OPERATOR_NAMES,
567
- ...Object.keys(customOperators ?? {})
568
- ]);
569
+ const known = /* @__PURE__ */ new Set([...BUILTIN_OPERATOR_NAMES, ...Object.keys(customOperators ?? {})]);
569
570
  const contracts = bundle.contracts ?? [];
570
571
  for (const contract of contracts) {
571
572
  const when = contract.when;
@@ -597,9 +598,7 @@ function _validateExpressionOperators(expr, known, contractId) {
597
598
  if (operator != null && typeof operator === "object") {
598
599
  for (const opName of Object.keys(operator)) {
599
600
  if (!known.has(opName)) {
600
- throw new EdictumConfigError(
601
- `Contract '${contractId}': unknown operator '${opName}'`
602
- );
601
+ throw new EdictumConfigError(`Contract '${contractId}': unknown operator '${opName}'`);
603
602
  }
604
603
  }
605
604
  }
@@ -734,7 +733,16 @@ function compilePost(contract, mode, customOps, customSels) {
734
733
  const meta = then.metadata ?? {};
735
734
  const check = (envelope, response) => {
736
735
  const outputText = response != null ? String(response) : void 0;
737
- return _evalAndVerdict(whenExpr, envelope, outputText, msgTpl, tags, meta, customOps, customSels);
736
+ return _evalAndVerdict(
737
+ whenExpr,
738
+ envelope,
739
+ outputText,
740
+ msgTpl,
741
+ tags,
742
+ meta,
743
+ customOps,
744
+ customSels
745
+ );
738
746
  };
739
747
  const effectValue = then.effect ?? "warn";
740
748
  const result = {
@@ -799,14 +807,18 @@ function mergeSessionLimits(contract, existing) {
799
807
  if ("max_tool_calls" in sessionLimits) {
800
808
  const raw = sessionLimits.max_tool_calls;
801
809
  if (typeof raw !== "number" || !Number.isFinite(raw)) {
802
- throw new EdictumConfigError(`Session limit max_tool_calls must be a finite number, got: ${String(raw)}`);
810
+ throw new EdictumConfigError(
811
+ `Session limit max_tool_calls must be a finite number, got: ${String(raw)}`
812
+ );
803
813
  }
804
814
  maxToolCalls = Math.min(maxToolCalls, raw);
805
815
  }
806
816
  if ("max_attempts" in sessionLimits) {
807
817
  const raw = sessionLimits.max_attempts;
808
818
  if (typeof raw !== "number" || !Number.isFinite(raw)) {
809
- throw new EdictumConfigError(`Session limit max_attempts must be a finite number, got: ${String(raw)}`);
819
+ throw new EdictumConfigError(
820
+ `Session limit max_attempts must be a finite number, got: ${String(raw)}`
821
+ );
810
822
  }
811
823
  maxAttempts = Math.min(maxAttempts, raw);
812
824
  }
@@ -828,10 +840,6 @@ function mergeSessionLimits(contract, existing) {
828
840
  return { maxAttempts, maxToolCalls, maxCallsPerTool };
829
841
  }
830
842
 
831
- // src/yaml-engine/sandbox-compiler.ts
832
- import { realpathSync } from "fs";
833
- import { resolve as pathResolve } from "path";
834
-
835
843
  // src/fnmatch.ts
836
844
  function fnmatch(name, pattern) {
837
845
  if (pattern === "*") return true;
@@ -856,6 +864,36 @@ function fnmatch(name, pattern) {
856
864
  return new RegExp("^" + regex + "$").test(safeName);
857
865
  }
858
866
 
867
+ // src/yaml-engine/resolve-path.ts
868
+ import { realpathSync } from "fs";
869
+ import { resolve as pathResolve, sep as pathSep, join as pathJoin } from "path";
870
+ function resolvePath(p) {
871
+ const cleaned = p.replace(/\0/g, "");
872
+ const resolved = pathResolve(cleaned);
873
+ try {
874
+ return realpathSync(resolved);
875
+ } catch (err) {
876
+ if (err.code !== "ENOENT") {
877
+ return resolved;
878
+ }
879
+ const parts = resolved.split(pathSep);
880
+ for (let i = parts.length - 1; i > 0; i--) {
881
+ const prefix = parts.slice(0, i).join(pathSep) || "/";
882
+ try {
883
+ const realPrefix = realpathSync(prefix);
884
+ const rest = parts.slice(i).join(pathSep);
885
+ return pathJoin(realPrefix, rest);
886
+ } catch (innerErr) {
887
+ if (innerErr.code !== "ENOENT") {
888
+ return resolved;
889
+ }
890
+ continue;
891
+ }
892
+ }
893
+ return resolved;
894
+ }
895
+ }
896
+
859
897
  // src/yaml-engine/sandbox-compiler.ts
860
898
  var _REDIRECT_PREFIX_RE = /^(?:\d*>>|>>|\d*>|>|<<|<)/;
861
899
  var _SHELL_SEPARATOR_RE = /[;|&\n\r`]|\$\(|\$\{|<\(/;
@@ -931,21 +969,25 @@ var _PATH_ARG_KEYS = /* @__PURE__ */ new Set([
931
969
  "destination",
932
970
  "source",
933
971
  "src",
934
- "dst"
972
+ "dst",
973
+ // Common path-like arg names from AI framework tool calls.
974
+ // IMPORTANT: Only include keys that are unambiguously path-related.
975
+ // Generic keys like 'name', 'input', 'output', 'from', 'to' are excluded
976
+ // because they frequently hold non-path values (e.g., { from: "English" }),
977
+ // and resolvePath() would produce false positives. The heuristic loop below
978
+ // catches path-like VALUES in any key (containing '..', '~', or '/').
979
+ "filename",
980
+ "file",
981
+ "filepath",
982
+ "read_path",
983
+ "write_path"
935
984
  ]);
936
- function _realpath(p) {
937
- try {
938
- return realpathSync(p);
939
- } catch {
940
- return pathResolve(p);
941
- }
942
- }
943
985
  function extractPaths(envelope) {
944
986
  const paths = [];
945
987
  const seen = /* @__PURE__ */ new Set();
946
988
  function add(p) {
947
989
  if (!p) return;
948
- const resolved = _realpath(p);
990
+ const resolved = resolvePath(p);
949
991
  if (!seen.has(resolved)) {
950
992
  seen.add(resolved);
951
993
  paths.push(resolved);
@@ -959,6 +1001,17 @@ function extractPaths(envelope) {
959
1001
  for (const [key, value] of Object.entries(args)) {
960
1002
  if (typeof value === "string" && value.startsWith("/") && !_PATH_ARG_KEYS.has(key)) add(value);
961
1003
  }
1004
+ for (const [key, value] of Object.entries(args)) {
1005
+ if (typeof value === "string" && !_PATH_ARG_KEYS.has(key)) {
1006
+ const isUrl = value.includes("://");
1007
+ if (!isUrl && value.includes("../") || // embedded traversal: foo/../etc/passwd
1008
+ value === ".." || // bare parent reference
1009
+ value.startsWith("../") || // parent traversal prefix: ../../etc/passwd
1010
+ value.startsWith("~") || value.startsWith("./") && !isUrl) {
1011
+ add(value);
1012
+ }
1013
+ }
1014
+ }
962
1015
  const cmd = envelope.bashCommand ?? args.command ?? "";
963
1016
  if (cmd) {
964
1017
  for (const token of tokenizeCommand(cmd)) {
@@ -1011,23 +1064,14 @@ function domainMatches(hostname, patterns) {
1011
1064
  }
1012
1065
 
1013
1066
  // src/yaml-engine/sandbox-compile-fn.ts
1014
- import { realpathSync as realpathSync2 } from "fs";
1015
- import { resolve as pathResolve2 } from "path";
1016
- function _realpath2(p) {
1017
- try {
1018
- return realpathSync2(p);
1019
- } catch {
1020
- return pathResolve2(p);
1021
- }
1022
- }
1023
1067
  function _pathWithin(filePath, prefix) {
1024
1068
  return filePath === prefix || filePath.startsWith(prefix.replace(/\/+$/, "") + "/");
1025
1069
  }
1026
1070
  function compileSandbox(contract, mode) {
1027
1071
  const contractId = contract.id;
1028
1072
  const toolPatterns = "tools" in contract ? contract.tools : [contract.tool];
1029
- const within = (contract.within ?? []).map(_realpath2);
1030
- const notWithin = (contract.not_within ?? []).map(_realpath2);
1073
+ const within = (contract.within ?? []).map(resolvePath);
1074
+ const notWithin = (contract.not_within ?? []).map(resolvePath);
1031
1075
  const allows = contract.allows ?? {};
1032
1076
  const notAllows = contract.not_allows ?? {};
1033
1077
  const allowedCommands = allows.commands ?? [];
@@ -1040,6 +1084,11 @@ function compileSandbox(contract, mode) {
1040
1084
  const check = (envelope) => {
1041
1085
  if (within.length > 0 || notWithin.length > 0) {
1042
1086
  const paths = extractPaths(envelope);
1087
+ if (paths.length === 0 && within.length > 0) {
1088
+ return Verdict.fail(
1089
+ expandMessage(messageTemplate, envelope) + " (no extractable paths \u2014 sandbox cannot verify boundary compliance)"
1090
+ );
1091
+ }
1043
1092
  if (paths.length > 0) {
1044
1093
  for (const p of paths) {
1045
1094
  for (const excluded of notWithin) {
@@ -1106,9 +1155,7 @@ function compileContracts(bundle, options = {}) {
1106
1155
  const customSels = options?.customSelectors ?? null;
1107
1156
  validateOperators(bundle, customOps);
1108
1157
  if (bundle.defaults == null || typeof bundle.defaults !== "object") {
1109
- throw new EdictumConfigError(
1110
- "Bundle missing required 'defaults' section with 'mode' field"
1111
- );
1158
+ throw new EdictumConfigError("Bundle missing required 'defaults' section with 'mode' field");
1112
1159
  }
1113
1160
  const defaults = bundle.defaults;
1114
1161
  const defaultMode = defaults.mode;
@@ -1154,7 +1201,8 @@ function compileContracts(bundle, options = {}) {
1154
1201
 
1155
1202
  // src/yaml-engine/loader.ts
1156
1203
  import { createHash } from "crypto";
1157
- import { readFileSync, realpathSync as realpathSync3, statSync } from "fs";
1204
+ import { readFileSync, realpathSync as realpathSync2, statSync } from "fs";
1205
+ import yaml from "js-yaml";
1158
1206
 
1159
1207
  // src/yaml-engine/loader-validators.ts
1160
1208
  function validateSchema(data) {
@@ -1175,13 +1223,22 @@ function validateSchema(data) {
1175
1223
  throw new EdictumConfigError("Schema validation failed: contracts must be an array");
1176
1224
  }
1177
1225
  }
1178
- var CONTROL_CHAR_RE = /[\x00-\x1f\x7f-\x9f]/;
1226
+ var CONTROL_CHAR_RE = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/;
1227
+ var CONTRACT_ID_RE = /^[a-z0-9][a-z0-9_-]*$/;
1179
1228
  function validateContractId(contractId) {
1229
+ if (contractId.length > 1e4) {
1230
+ throw new EdictumConfigError("Contract id exceeds maximum length");
1231
+ }
1180
1232
  if (CONTROL_CHAR_RE.test(contractId)) {
1181
1233
  throw new EdictumConfigError(
1182
1234
  `Contract id contains control characters: '${contractId.replace(CONTROL_CHAR_RE, "\\x??")}'`
1183
1235
  );
1184
1236
  }
1237
+ if (!CONTRACT_ID_RE.test(contractId)) {
1238
+ throw new EdictumConfigError(
1239
+ `Contract id '${contractId}' must match pattern ^[a-z0-9][a-z0-9_-]*$`
1240
+ );
1241
+ }
1185
1242
  }
1186
1243
  function validateUniqueIds(data) {
1187
1244
  const ids = /* @__PURE__ */ new Set();
@@ -1282,26 +1339,229 @@ function validateSandboxContracts(data) {
1282
1339
  }
1283
1340
  }
1284
1341
 
1342
+ // src/yaml-engine/loader-field-validators.ts
1343
+ var VALID_CONTRACT_TYPES = /* @__PURE__ */ new Set(["pre", "post", "session", "sandbox"]);
1344
+ var PRE_EFFECTS = /* @__PURE__ */ new Set(["deny", "approve"]);
1345
+ var POST_EFFECTS = /* @__PURE__ */ new Set(["warn", "redact", "deny"]);
1346
+ var VALID_MODES = /* @__PURE__ */ new Set(["enforce", "observe"]);
1347
+ var VALID_SIDE_EFFECTS = /* @__PURE__ */ new Set(["pure", "read", "write", "irreversible"]);
1348
+ var KNOWN_TOP_LEVEL = /* @__PURE__ */ new Set([
1349
+ "apiVersion",
1350
+ "kind",
1351
+ "metadata",
1352
+ "defaults",
1353
+ "contracts",
1354
+ "tools",
1355
+ "observability",
1356
+ "observe_alongside"
1357
+ ]);
1358
+ var METADATA_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
1359
+ var MAX_MESSAGE_LENGTH = 500;
1360
+ function fail(msg) {
1361
+ throw new EdictumConfigError(`Schema validation failed: ${msg}`);
1362
+ }
1363
+ function validateContractFields(data) {
1364
+ for (const key of Object.keys(data)) {
1365
+ if (!KNOWN_TOP_LEVEL.has(key)) fail(`unknown top-level field '${key}'`);
1366
+ }
1367
+ if (data.metadata == null || typeof data.metadata !== "object" || Array.isArray(data.metadata)) {
1368
+ fail("'metadata' is required and must be an object");
1369
+ }
1370
+ const meta = data.metadata;
1371
+ if (meta.name == null || typeof meta.name !== "string") fail("metadata.name is required");
1372
+ const metaName = meta.name;
1373
+ if (metaName.length > 1e4) fail("metadata.name exceeds maximum length");
1374
+ if (!METADATA_NAME_RE.test(metaName)) {
1375
+ fail(
1376
+ `metadata.name must be a lowercase slug (^[a-z0-9][a-z0-9._-]*$), got '${metaName.slice(0, 100)}'`
1377
+ );
1378
+ }
1379
+ if (data.defaults == null || typeof data.defaults !== "object" || Array.isArray(data.defaults)) {
1380
+ fail("'defaults' is required and must be an object");
1381
+ }
1382
+ const mode = data.defaults.mode;
1383
+ if (!VALID_MODES.has(mode)) {
1384
+ fail(`defaults.mode must be 'enforce' or 'observe', got '${String(mode)}'`);
1385
+ }
1386
+ const contracts = data.contracts;
1387
+ if (contracts.length === 0) fail("contracts must contain at least 1 item");
1388
+ if (data.tools != null) {
1389
+ if (typeof data.tools !== "object" || Array.isArray(data.tools)) {
1390
+ fail("'tools' must be a mapping of tool names to descriptors, not an array");
1391
+ }
1392
+ for (const [tn, td] of Object.entries(data.tools)) {
1393
+ if (td == null || typeof td !== "object" || Array.isArray(td)) {
1394
+ fail(`tools.${tn} must be an object with a 'side_effect' field`);
1395
+ }
1396
+ const se = td.side_effect;
1397
+ if (!VALID_SIDE_EFFECTS.has(se)) {
1398
+ fail(
1399
+ `tools.${tn}.side_effect must be 'pure', 'read', 'write', or 'irreversible', got '${String(se)}'`
1400
+ );
1401
+ }
1402
+ }
1403
+ }
1404
+ for (const c of contracts) {
1405
+ if (c == null || typeof c !== "object" || Array.isArray(c)) {
1406
+ fail("every contract must be an object (got null or non-object array element)");
1407
+ }
1408
+ if (c.id == null || typeof c.id !== "string" || c.id.length === 0) {
1409
+ fail("every contract requires a non-empty 'id' string");
1410
+ }
1411
+ const cid = c.id;
1412
+ if (!VALID_CONTRACT_TYPES.has(c.type)) {
1413
+ fail(`contract '${cid}': invalid type '${String(c.type)}'`);
1414
+ }
1415
+ const t = c.type;
1416
+ if (t === "pre" || t === "post") validatePrePost(c, t, cid);
1417
+ else if (t === "session") validateSession(c, cid);
1418
+ else if (t === "sandbox") validateSandboxStructure(c, cid);
1419
+ }
1420
+ }
1421
+ function validatePrePost(c, t, cid) {
1422
+ if (c.tool == null || typeof c.tool !== "string") {
1423
+ fail(`${t} contract '${cid}' requires 'tool' to be a string`);
1424
+ }
1425
+ if (c.when == null || typeof c.when !== "object" || Array.isArray(c.when)) {
1426
+ fail(
1427
+ `${t} contract '${cid}' requires 'when' to be a mapping (got ${Array.isArray(c.when) ? "array" : typeof c.when})`
1428
+ );
1429
+ }
1430
+ if (c.then == null || typeof c.then !== "object" || Array.isArray(c.then)) {
1431
+ fail(`${t} contract '${cid}' requires 'then' to be a mapping`);
1432
+ }
1433
+ const then = c.then;
1434
+ if (then.effect == null) fail(`${t} contract '${cid}' requires 'then.effect'`);
1435
+ if (then.message == null) fail(`${t} contract '${cid}' requires 'then.message'`);
1436
+ validateMessageLength(then.message, `${t} contract '${cid}'`);
1437
+ const effect = then.effect;
1438
+ if (t === "pre" && !PRE_EFFECTS.has(effect)) {
1439
+ fail(`pre contract '${cid}': effect must be 'deny' or 'approve', got '${effect}'`);
1440
+ }
1441
+ if (t === "post" && !POST_EFFECTS.has(effect)) {
1442
+ fail(`post contract '${cid}': effect must be 'warn', 'redact', or 'deny', got '${effect}'`);
1443
+ }
1444
+ }
1445
+ function validateSession(c, cid) {
1446
+ if (c.limits == null || typeof c.limits !== "object" || Array.isArray(c.limits)) {
1447
+ fail(`session contract '${cid}' requires 'limits' to be a mapping`);
1448
+ }
1449
+ const lim = c.limits;
1450
+ if (!("max_tool_calls" in lim) && !("max_attempts" in lim) && !("max_calls_per_tool" in lim)) {
1451
+ fail(
1452
+ `session contract '${cid}': limits must have max_tool_calls, max_attempts, or max_calls_per_tool`
1453
+ );
1454
+ }
1455
+ if (c.then == null || typeof c.then !== "object" || Array.isArray(c.then)) {
1456
+ fail(`session contract '${cid}' requires 'then' to be a mapping`);
1457
+ }
1458
+ const then = c.then;
1459
+ if (then.effect !== "deny") {
1460
+ fail(`session contract '${cid}': effect must be 'deny', got '${String(then.effect)}'`);
1461
+ }
1462
+ if (then.message == null) fail(`session contract '${cid}' requires 'then.message'`);
1463
+ validateMessageLength(then.message, `session contract '${cid}'`);
1464
+ }
1465
+ function validateSandboxStructure(c, cid) {
1466
+ if (c.tool == null && c.tools == null) {
1467
+ fail(`sandbox contract '${cid}' requires either 'tool' or 'tools'`);
1468
+ }
1469
+ if (c.tool != null && typeof c.tool !== "string") {
1470
+ fail(`sandbox contract '${cid}': 'tool' must be a string`);
1471
+ }
1472
+ if (c.tools != null && (!Array.isArray(c.tools) || c.tools.length === 0)) {
1473
+ fail(`sandbox contract '${cid}': 'tools' must be a non-empty array`);
1474
+ }
1475
+ if (c.within == null && c.allows == null) {
1476
+ fail(`sandbox contract '${cid}' requires either 'within' or 'allows'`);
1477
+ }
1478
+ if (c.within != null && (!Array.isArray(c.within) || c.within.length === 0)) {
1479
+ fail(`sandbox contract '${cid}': 'within' must be a non-empty array`);
1480
+ }
1481
+ if (c.message == null) fail(`sandbox contract '${cid}' requires 'message'`);
1482
+ validateMessageLength(c.message, `sandbox contract '${cid}'`);
1483
+ }
1484
+ function validateMessageLength(msg, context) {
1485
+ if (typeof msg !== "string") {
1486
+ fail(`${context}: message must be a string`);
1487
+ }
1488
+ if (msg.length > MAX_MESSAGE_LENGTH) {
1489
+ fail(`${context}: message exceeds ${MAX_MESSAGE_LENGTH} characters`);
1490
+ }
1491
+ }
1492
+
1493
+ // src/yaml-engine/loader-expression-validators.ts
1494
+ var NUMERIC_OPS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
1495
+ var STRING_OPS = /* @__PURE__ */ new Set(["contains", "starts_with", "ends_with"]);
1496
+ var ARRAY_MIN1_OPS = /* @__PURE__ */ new Set(["in", "not_in", "contains_any", "matches_any"]);
1497
+ function fail2(msg) {
1498
+ throw new EdictumConfigError(`Schema validation failed: ${msg}`);
1499
+ }
1500
+ function validateExpressionShapes(data) {
1501
+ for (const c of data.contracts ?? []) {
1502
+ if (c == null || typeof c !== "object") continue;
1503
+ const contract = c;
1504
+ if (contract.when != null) checkExprShape(contract.when, contract.id ?? "?");
1505
+ }
1506
+ }
1507
+ var MAX_EXPR_DEPTH = 50;
1508
+ function checkExprShape(expr, cid, depth = 0) {
1509
+ if (depth > MAX_EXPR_DEPTH)
1510
+ fail2(`contract '${cid}': expression nesting exceeds maximum depth (${MAX_EXPR_DEPTH})`);
1511
+ if (expr == null || typeof expr !== "object") return;
1512
+ const e = expr;
1513
+ if ("all" in e) {
1514
+ const a = e.all;
1515
+ if (!Array.isArray(a) || a.length === 0)
1516
+ fail2(`contract '${cid}': 'all' requires a non-empty array`);
1517
+ for (const s of a) checkExprShape(s, cid, depth + 1);
1518
+ return;
1519
+ }
1520
+ if ("any" in e) {
1521
+ const a = e.any;
1522
+ if (!Array.isArray(a) || a.length === 0)
1523
+ fail2(`contract '${cid}': 'any' requires a non-empty array`);
1524
+ for (const s of a) checkExprShape(s, cid, depth + 1);
1525
+ return;
1526
+ }
1527
+ if ("not" in e) {
1528
+ if (e.not == null || typeof e.not !== "object" || Array.isArray(e.not)) {
1529
+ fail2(
1530
+ `contract '${cid}': 'not' requires an expression mapping (got ${e.not === null ? "null" : Array.isArray(e.not) ? "array" : typeof e.not})`
1531
+ );
1532
+ }
1533
+ checkExprShape(e.not, cid, depth + 1);
1534
+ return;
1535
+ }
1536
+ for (const v of Object.values(e)) {
1537
+ if (v == null || typeof v !== "object") continue;
1538
+ if (Array.isArray(v)) {
1539
+ fail2(`contract '${cid}': selector value must be an operator mapping, not an array`);
1540
+ }
1541
+ const op = v;
1542
+ for (const [name, val] of Object.entries(op)) {
1543
+ if (NUMERIC_OPS.has(name) && typeof val !== "number") {
1544
+ fail2(`contract '${cid}': operator '${name}' requires a number, got ${typeof val}`);
1545
+ }
1546
+ if (STRING_OPS.has(name) && typeof val !== "string") {
1547
+ fail2(`contract '${cid}': operator '${name}' requires a string, got ${typeof val}`);
1548
+ }
1549
+ if (ARRAY_MIN1_OPS.has(name) && (!Array.isArray(val) || val.length === 0)) {
1550
+ fail2(`contract '${cid}': operator '${name}' requires a non-empty array`);
1551
+ }
1552
+ }
1553
+ }
1554
+ }
1555
+
1285
1556
  // src/yaml-engine/loader.ts
1286
1557
  var MAX_BUNDLE_SIZE = 1048576;
1287
1558
  function computeHash(rawBytes) {
1288
1559
  return { hex: createHash("sha256").update(rawBytes).digest("hex") };
1289
1560
  }
1290
- function requireYaml() {
1291
- try {
1292
- const yaml = __require("js-yaml");
1293
- return yaml;
1294
- } catch {
1295
- throw new EdictumConfigError(
1296
- "The YAML engine requires js-yaml. Install it with: npm install js-yaml"
1297
- );
1298
- }
1299
- }
1300
1561
  function parseYaml(content) {
1301
- const yaml = requireYaml();
1302
1562
  let data;
1303
1563
  try {
1304
- data = yaml.load(content);
1564
+ data = yaml.load(content, { schema: yaml.CORE_SCHEMA });
1305
1565
  } catch (e) {
1306
1566
  throw new EdictumConfigError(`YAML parse error: ${String(e)}`);
1307
1567
  }
@@ -1312,13 +1572,15 @@ function parseYaml(content) {
1312
1572
  }
1313
1573
  function validateBundle(data) {
1314
1574
  validateSchema(data);
1575
+ validateContractFields(data);
1315
1576
  validateUniqueIds(data);
1577
+ validateExpressionShapes(data);
1316
1578
  validateRegexes(data);
1317
1579
  validatePreSelectors(data);
1318
1580
  validateSandboxContracts(data);
1319
1581
  }
1320
1582
  function loadBundle(source) {
1321
- const resolved = realpathSync3(source);
1583
+ const resolved = realpathSync2(source);
1322
1584
  const fileSize = statSync(resolved).size;
1323
1585
  if (fileSize > MAX_BUNDLE_SIZE) {
1324
1586
  throw new EdictumConfigError(
@@ -1373,9 +1635,10 @@ function fromYaml(...args) {
1373
1635
  policyVersion = entry[1].hex;
1374
1636
  report = { overriddenContracts: [], observeContracts: [] };
1375
1637
  } else {
1376
- const bundleTuples = loaded.map(
1377
- ([data], i) => [data, paths[i]]
1378
- );
1638
+ const bundleTuples = loaded.map(([data], i) => [
1639
+ data,
1640
+ paths[i]
1641
+ ]);
1379
1642
  const composed = composeBundles(...bundleTuples);
1380
1643
  bundleData = composed.bundle;
1381
1644
  report = composed.report;
@@ -1496,15 +1759,9 @@ var Edictum = class _Edictum {
1496
1759
  this._approvalBackend = options.approvalBackend ?? null;
1497
1760
  this._localSink = new CollectingAuditSink();
1498
1761
  if (Array.isArray(options.auditSink)) {
1499
- this.auditSink = new CompositeSink([
1500
- this._localSink,
1501
- ...options.auditSink
1502
- ]);
1762
+ this.auditSink = new CompositeSink([this._localSink, ...options.auditSink]);
1503
1763
  } else if (options.auditSink != null) {
1504
- this.auditSink = new CompositeSink([
1505
- this._localSink,
1506
- options.auditSink
1507
- ]);
1764
+ this.auditSink = new CompositeSink([this._localSink, options.auditSink]);
1508
1765
  } else {
1509
1766
  this.auditSink = this._localSink;
1510
1767
  }
@@ -1602,24 +1859,16 @@ var Edictum = class _Edictum {
1602
1859
  }
1603
1860
  getHooks(phase, envelope) {
1604
1861
  const hooks = phase === "before" ? this._beforeHooks : this._afterHooks;
1605
- return hooks.filter(
1606
- (h) => h.tool === "*" || fnmatch(envelope.toolName, h.tool)
1607
- );
1862
+ return hooks.filter((h) => h.tool === "*" || fnmatch(envelope.toolName, h.tool));
1608
1863
  }
1609
1864
  // -----------------------------------------------------------------------
1610
1865
  // Contract accessors -- enforce mode
1611
1866
  // -----------------------------------------------------------------------
1612
1867
  getPreconditions(envelope) {
1613
- return _Edictum._filterByTool(
1614
- this._state.preconditions,
1615
- envelope
1616
- );
1868
+ return _Edictum._filterByTool(this._state.preconditions, envelope);
1617
1869
  }
1618
1870
  getPostconditions(envelope) {
1619
- return _Edictum._filterByTool(
1620
- this._state.postconditions,
1621
- envelope
1622
- );
1871
+ return _Edictum._filterByTool(this._state.postconditions, envelope);
1623
1872
  }
1624
1873
  getSessionContracts() {
1625
1874
  return [...this._state.sessionContracts];
@@ -1679,12 +1928,16 @@ var Edictum = class _Edictum {
1679
1928
  const edictumType = raw._edictum_type;
1680
1929
  const isObserve = raw._edictum_observe ?? raw._edictum_shadow ?? false;
1681
1930
  if (edictumType != null) {
1682
- _Edictum._classifyInternal(
1683
- raw,
1684
- edictumType,
1685
- isObserve,
1686
- { pre, post, session, sandbox, oPre, oPost, oSession, oSandbox }
1687
- );
1931
+ _Edictum._classifyInternal(raw, edictumType, isObserve, {
1932
+ pre,
1933
+ post,
1934
+ session,
1935
+ sandbox,
1936
+ oPre,
1937
+ oPost,
1938
+ oSession,
1939
+ oSandbox
1940
+ });
1688
1941
  } else if (isSessionContract(item)) {
1689
1942
  const name = raw.name ?? "anonymous";
1690
1943
  session.push({
@@ -1742,9 +1995,12 @@ var Edictum = class _Edictum {
1742
1995
  static _classifyInternal(raw, edictumType, isObserve, lists) {
1743
1996
  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 };
1744
1997
  if (edictumType === "precondition") target.pre.push(raw);
1745
- else if (edictumType === "postcondition") target.post.push(raw);
1746
- else if (edictumType === "session_contract") target.session.push(raw);
1747
- else if (edictumType === "sandbox") target.sandbox.push(raw);
1998
+ else if (edictumType === "postcondition")
1999
+ target.post.push(raw);
2000
+ else if (edictumType === "session_contract")
2001
+ target.session.push(raw);
2002
+ else if (edictumType === "sandbox")
2003
+ target.sandbox.push(raw);
1748
2004
  else {
1749
2005
  throw new EdictumConfigError(
1750
2006
  `Unknown _edictum_type "${edictumType}". Expected "precondition", "postcondition", "session_contract", or "sandbox".`
@@ -1786,7 +2042,7 @@ var Edictum = class _Edictum {
1786
2042
  // -----------------------------------------------------------------------
1787
2043
  /** Execute a tool call with full governance pipeline. */
1788
2044
  async run(toolName, args, toolCallable, options) {
1789
- const { run: run2 } = await import("./runner-ASI4JIW2.mjs");
2045
+ const { run: run2 } = await import("./runner-JCAQMF6O.mjs");
1790
2046
  return run2(this, toolName, args, toolCallable, options);
1791
2047
  }
1792
2048
  /**
@@ -1796,15 +2052,11 @@ var Edictum = class _Edictum {
1796
2052
  * Session contracts are skipped.
1797
2053
  */
1798
2054
  evaluate(toolName, args, options) {
1799
- return import("./dry-run-54PYIM6T.mjs").then(
1800
- ({ evaluate }) => evaluate(this, toolName, args, options)
1801
- );
2055
+ return import("./dry-run-JTRNTZA5.mjs").then(({ evaluate }) => evaluate(this, toolName, args, options));
1802
2056
  }
1803
2057
  /** Evaluate a batch of tool calls. Thin wrapper over evaluate(). */
1804
2058
  evaluateBatch(calls) {
1805
- return import("./dry-run-54PYIM6T.mjs").then(
1806
- ({ evaluateBatch }) => evaluateBatch(this, calls)
1807
- );
2059
+ return import("./dry-run-JTRNTZA5.mjs").then(({ evaluateBatch }) => evaluateBatch(this, calls));
1808
2060
  }
1809
2061
  static fromYaml(...args) {
1810
2062
  return fromYaml(...args);