@sunaiva/gate 1.0.0 → 1.1.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.
Files changed (76) hide show
  1. package/BUSINESS_LICENSE.md +70 -0
  2. package/CHANGELOG.md +148 -0
  3. package/LICENSE +0 -0
  4. package/README.md +411 -27
  5. package/dist/config/defaults.d.ts +22 -1
  6. package/dist/config/defaults.d.ts.map +1 -1
  7. package/dist/config/defaults.js +56 -8
  8. package/dist/config/defaults.js.map +1 -1
  9. package/dist/config/loader.d.ts +0 -0
  10. package/dist/config/loader.d.ts.map +1 -1
  11. package/dist/config/loader.js +24 -6
  12. package/dist/config/loader.js.map +1 -1
  13. package/dist/engine/backend-client.d.ts +58 -0
  14. package/dist/engine/backend-client.d.ts.map +1 -0
  15. package/dist/engine/backend-client.js +287 -0
  16. package/dist/engine/backend-client.js.map +1 -0
  17. package/dist/engine/hmac-verifier.d.ts +33 -0
  18. package/dist/engine/hmac-verifier.d.ts.map +1 -0
  19. package/dist/engine/hmac-verifier.js +161 -0
  20. package/dist/engine/hmac-verifier.js.map +1 -0
  21. package/dist/engine/immutability.d.ts +59 -0
  22. package/dist/engine/immutability.d.ts.map +1 -0
  23. package/dist/engine/immutability.js +129 -0
  24. package/dist/engine/immutability.js.map +1 -0
  25. package/dist/engine/pattern-matcher.d.ts +13 -0
  26. package/dist/engine/pattern-matcher.d.ts.map +1 -1
  27. package/dist/engine/pattern-matcher.js +85 -17
  28. package/dist/engine/pattern-matcher.js.map +1 -1
  29. package/dist/engine/rule-engine.d.ts +62 -1
  30. package/dist/engine/rule-engine.d.ts.map +1 -1
  31. package/dist/engine/rule-engine.js +222 -12
  32. package/dist/engine/rule-engine.js.map +1 -1
  33. package/dist/engine/session-state.d.ts +0 -0
  34. package/dist/engine/session-state.d.ts.map +1 -1
  35. package/dist/engine/session-state.js +8 -2
  36. package/dist/engine/session-state.js.map +1 -1
  37. package/dist/engine/ship-confidence-gate.d.ts +184 -0
  38. package/dist/engine/ship-confidence-gate.d.ts.map +1 -0
  39. package/dist/engine/ship-confidence-gate.js +768 -0
  40. package/dist/engine/ship-confidence-gate.js.map +1 -0
  41. package/dist/index.d.ts +0 -0
  42. package/dist/index.d.ts.map +0 -0
  43. package/dist/index.js +289 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/rules/categories.json +0 -0
  46. package/dist/rules/presets.json +0 -0
  47. package/dist/rules/rules.json +200 -100
  48. package/dist/tools/audit.d.ts +6 -0
  49. package/dist/tools/audit.d.ts.map +1 -1
  50. package/dist/tools/audit.js +43 -6
  51. package/dist/tools/audit.js.map +1 -1
  52. package/dist/tools/bypass.d.ts +0 -0
  53. package/dist/tools/bypass.d.ts.map +1 -1
  54. package/dist/tools/bypass.js +50 -6
  55. package/dist/tools/bypass.js.map +1 -1
  56. package/dist/tools/rules.d.ts +0 -0
  57. package/dist/tools/rules.d.ts.map +0 -0
  58. package/dist/tools/rules.js +0 -0
  59. package/dist/tools/rules.js.map +0 -0
  60. package/dist/tools/ship-confidence.d.ts +11 -0
  61. package/dist/tools/ship-confidence.d.ts.map +1 -0
  62. package/dist/tools/ship-confidence.js +42 -0
  63. package/dist/tools/ship-confidence.js.map +1 -0
  64. package/dist/tools/update.d.ts +0 -0
  65. package/dist/tools/update.d.ts.map +1 -1
  66. package/dist/tools/update.js +45 -9
  67. package/dist/tools/update.js.map +1 -1
  68. package/dist/tools/validate.d.ts +0 -0
  69. package/dist/tools/validate.d.ts.map +1 -1
  70. package/dist/tools/validate.js +56 -4
  71. package/dist/tools/validate.js.map +1 -1
  72. package/dist/types/backend.d.ts +69 -0
  73. package/dist/types/backend.d.ts.map +1 -0
  74. package/dist/types/backend.js +18 -0
  75. package/dist/types/backend.js.map +1 -0
  76. package/package.json +11 -3
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Immutability guard — constitutional rules cannot be disabled or bypassed.
3
+ *
4
+ * Builder B4. Resolves CRITICAL findings:
5
+ * - C2: `update_rules({disable: ['fin-001']})` must reject (cannot disable constitutional).
6
+ * - C3: `log_bypass({rule_id: 'fin-001'})` must reject (cannot bypass constitutional).
7
+ *
8
+ * Design:
9
+ * - Single canonical source of truth for "what is constitutional":
10
+ * 1. CONSTITUTIONAL_RULE_IDS (frozen array in src/config/defaults.ts) — primary.
11
+ * 2. Cross-check against loadConstitutionalRulesOnly() from rule-engine — defense in depth.
12
+ * Union of both => guard set. This means a future rule with `enforcement === "constitutional"`
13
+ * in rules.json is automatically protected even if CONSTITUTIONAL_RULE_IDS isn't updated.
14
+ * - assertCanDisable() and assertCanBypass() throw ConstitutionalImmutableError.
15
+ * - enforceConstitutionalActive() returns a config with every constitutional ID guaranteed
16
+ * in active_rules — called on every config load so user tampering of ~/.sunaiva/gate-config.json
17
+ * cannot disable a constitutional rule.
18
+ *
19
+ * Why both sources?
20
+ * - CONSTITUTIONAL_RULE_IDS in defaults.ts is the frozen, code-level source — even if
21
+ * the bundled rules.json is missing or corrupt, we still know which IDs are protected.
22
+ * - loadConstitutionalRulesOnly() from rule-engine reads from the package-bundled rules,
23
+ * never user config — adds new rules without code changes if rules.json is updated.
24
+ * - Union ensures additive protection: a rule listed EITHER place is constitutional.
25
+ */
26
+ import { CONSTITUTIONAL_RULE_IDS } from "../config/defaults.js";
27
+ import { loadConstitutionalRulesOnly } from "./rule-engine.js";
28
+ // ----------------------------------------------------------------------
29
+ // Errors
30
+ // ----------------------------------------------------------------------
31
+ export class ConstitutionalImmutableError extends Error {
32
+ rule_id;
33
+ op;
34
+ constructor(ruleId, op) {
35
+ const verb = op === "disable" ? "disabled" : "bypassed";
36
+ super(`Constitutional rule '${ruleId}' cannot be ${verb}.`);
37
+ this.name = "ConstitutionalImmutableError";
38
+ this.rule_id = ruleId;
39
+ this.op = op;
40
+ // Restore prototype chain for `instanceof` to work across compile targets.
41
+ Object.setPrototypeOf(this, ConstitutionalImmutableError.prototype);
42
+ }
43
+ }
44
+ // ----------------------------------------------------------------------
45
+ // Canonical constitutional set (cached)
46
+ // ----------------------------------------------------------------------
47
+ let _cached = null;
48
+ /**
49
+ * Returns the canonical set of constitutional rule IDs. Union of:
50
+ * 1. CONSTITUTIONAL_RULE_IDS (frozen array — code-level guarantee)
51
+ * 2. loadConstitutionalRulesOnly() (rules.json — runtime additive)
52
+ *
53
+ * Cached after first call. Tests can force a refresh with refreshConstitutionalCache().
54
+ */
55
+ export function getConstitutionalRuleIds() {
56
+ if (_cached)
57
+ return _cached;
58
+ const set = new Set(CONSTITUTIONAL_RULE_IDS);
59
+ // Defense in depth: also pick up any rule with enforcement === "constitutional"
60
+ // in the bundled rules.json, even if the frozen array hasn't been updated yet.
61
+ try {
62
+ for (const r of loadConstitutionalRulesOnly()) {
63
+ if (r?.id)
64
+ set.add(r.id);
65
+ }
66
+ }
67
+ catch {
68
+ // Fail-OPEN on the rules.json side — the frozen array is still authoritative.
69
+ }
70
+ _cached = set;
71
+ return _cached;
72
+ }
73
+ /**
74
+ * Test-only cache reset. Used by tests that mutate environment / config files
75
+ * and need a fresh read.
76
+ */
77
+ export function refreshConstitutionalCache() {
78
+ _cached = null;
79
+ }
80
+ // ----------------------------------------------------------------------
81
+ // Predicate
82
+ // ----------------------------------------------------------------------
83
+ export function isConstitutional(ruleId) {
84
+ return getConstitutionalRuleIds().has(ruleId);
85
+ }
86
+ // ----------------------------------------------------------------------
87
+ // Assertions (throw on violation)
88
+ // ----------------------------------------------------------------------
89
+ export function assertCanDisable(ruleId) {
90
+ if (isConstitutional(ruleId)) {
91
+ throw new ConstitutionalImmutableError(ruleId, "disable");
92
+ }
93
+ }
94
+ export function assertCanBypass(ruleId) {
95
+ if (isConstitutional(ruleId)) {
96
+ throw new ConstitutionalImmutableError(ruleId, "bypass");
97
+ }
98
+ }
99
+ // ----------------------------------------------------------------------
100
+ // Config re-merge — load-time immutability
101
+ // ----------------------------------------------------------------------
102
+ /**
103
+ * Returns a new GateConfig with every constitutional rule ID guaranteed
104
+ * in active_rules. The on-disk config file is NOT mutated; this is an
105
+ * in-memory enforcement layer so a user can hand-edit `~/.sunaiva/gate-config.json`
106
+ * to remove constitutional rules and the next runtime load will simply
107
+ * re-add them — silently and unbypassably.
108
+ *
109
+ * Idempotent: calling on a config that already has all constitutional IDs
110
+ * is a no-op (returns a structurally equal config).
111
+ */
112
+ export function enforceConstitutionalActive(config) {
113
+ const required = getConstitutionalRuleIds();
114
+ const active = new Set(config.active_rules ?? []);
115
+ let added = false;
116
+ for (const id of required) {
117
+ if (!active.has(id)) {
118
+ active.add(id);
119
+ added = true;
120
+ }
121
+ }
122
+ if (!added)
123
+ return config;
124
+ return {
125
+ ...config,
126
+ active_rules: [...active],
127
+ };
128
+ }
129
+ //# sourceMappingURL=immutability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"immutability.js","sourceRoot":"","sources":["../../src/engine/immutability.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,19 @@
1
1
  /**
2
2
  * Pattern Matcher — local regex/keyword matching for rule detection_pattern fields.
3
3
  * No API call. Runs entirely in-process.
4
+ *
5
+ * Matching strategy (v1.0.1):
6
+ * 1. Normalize both action and keyword: lowercase, replace . / - _ ( ) = with space,
7
+ * collapse runs of whitespace.
8
+ * 2. Split the normalized keyword into tokens.
9
+ * 3. Expand "/" alternatives in the original keyword (e.g. "main/master" → ["main","master"]).
10
+ * 4. Match if the action contains the keyword's distinctive token sequence,
11
+ * or all distinctive tokens of the keyword in order with flexible whitespace.
12
+ *
13
+ * This catches real agent inputs like:
14
+ * - "git push origin main" via keyword "git push to main/master"
15
+ * - "stripe.charges.create()" via keyword "stripe charges"
16
+ * - "rm -rf /" via keyword "rm -rf commands"
4
17
  */
5
18
  export interface MatchResult {
6
19
  matched: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"pattern-matcher.d.ts","sourceRoot":"","sources":["../../src/engine/pattern-matcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;CACvC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,EAAE,CAexE;AAeD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,GACvB,WAAW,CAwBb;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC,GACtD,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAM1B"}
1
+ {"version":3,"file":"pattern-matcher.d.ts","sourceRoot":"","sources":["../../src/engine/pattern-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;CACvC;AA4CD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBxE;AAuBD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,GACvB,WAAW,CA6Bb;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC,GACtD,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAM1B"}
@@ -1,7 +1,59 @@
1
1
  /**
2
2
  * Pattern Matcher — local regex/keyword matching for rule detection_pattern fields.
3
3
  * No API call. Runs entirely in-process.
4
+ *
5
+ * Matching strategy (v1.0.1):
6
+ * 1. Normalize both action and keyword: lowercase, replace . / - _ ( ) = with space,
7
+ * collapse runs of whitespace.
8
+ * 2. Split the normalized keyword into tokens.
9
+ * 3. Expand "/" alternatives in the original keyword (e.g. "main/master" → ["main","master"]).
10
+ * 4. Match if the action contains the keyword's distinctive token sequence,
11
+ * or all distinctive tokens of the keyword in order with flexible whitespace.
12
+ *
13
+ * This catches real agent inputs like:
14
+ * - "git push origin main" via keyword "git push to main/master"
15
+ * - "stripe.charges.create()" via keyword "stripe charges"
16
+ * - "rm -rf /" via keyword "rm -rf commands"
17
+ */
18
+ // Stop words that are filler in human-readable rule patterns and should not be
19
+ // required for a match to succeed.
20
+ const STOP_WORDS = new Set([
21
+ "the", "a", "an", "to", "of", "in", "on", "for", "and", "or", "with",
22
+ "without", "from", "into", "via", "by", "is", "are", "be", "as", "at",
23
+ "commands", "patterns", "calls", "operations", "actions",
24
+ ]);
25
+ function normalize(text) {
26
+ return text
27
+ .toLowerCase()
28
+ .replace(/[./\\\-_()=,;:'"`]/g, " ")
29
+ .replace(/\s+/g, " ")
30
+ .trim();
31
+ }
32
+ function tokenize(text) {
33
+ return normalize(text).split(" ").filter((t) => t.length > 0);
34
+ }
35
+ function distinctiveTokens(keyword) {
36
+ return tokenize(keyword).filter((t) => !STOP_WORDS.has(t) && t.length > 1);
37
+ }
38
+ /**
39
+ * Expand a keyword's "/" alternatives. "main/master" → ["main", "master"].
40
+ * For multi-word keywords containing "/", expand each slash-segment in turn,
41
+ * keeping the surrounding context.
42
+ * "git push to main/master" → ["git push to main", "git push to master"]
4
43
  */
44
+ function expandSlashAlternatives(keyword) {
45
+ if (!keyword.includes("/"))
46
+ return [keyword];
47
+ // Find first "/" segment with surrounding non-space chars
48
+ const m = keyword.match(/(\S+\/\S+)/);
49
+ if (!m)
50
+ return [keyword];
51
+ const segment = m[1];
52
+ const alternatives = segment.split("/");
53
+ const expanded = alternatives.map((alt) => keyword.replace(segment, alt));
54
+ // Recurse in case multiple "/" segments
55
+ return expanded.flatMap(expandSlashAlternatives);
56
+ }
5
57
  /**
6
58
  * Extract keyword phrases from a human-readable detection_pattern string.
7
59
  * The pattern field is prose like:
@@ -10,28 +62,40 @@
10
62
  export function parseDetectionPattern(detectionPattern) {
11
63
  // Strip "Detects:" prefix if present
12
64
  const cleaned = detectionPattern.replace(/^Detects:\s*/i, "");
13
- // Split on commas, newlines, semicolons
65
+ // Split on commas, newlines, semicolons (top-level separators between phrases)
14
66
  const fragments = cleaned.split(/[,;\n]+/);
15
67
  const keywords = [];
16
68
  for (const fragment of fragments) {
17
- const trimmed = fragment.trim().toLowerCase();
69
+ const trimmed = fragment.trim();
18
70
  if (trimmed.length > 2) {
19
- keywords.push(trimmed);
71
+ // Expand slash alternatives so "main/master" produces both forms
72
+ for (const expanded of expandSlashAlternatives(trimmed)) {
73
+ keywords.push(expanded);
74
+ }
20
75
  }
21
76
  }
22
77
  return keywords;
23
78
  }
24
79
  /**
25
- * Build regex patterns from a list of keyword phrases.
26
- * Each phrase becomes a word-boundary regex.
80
+ * Build a regex that matches a keyword's distinctive tokens against the
81
+ * normalized action, in order, with up to 3 intermediate tokens allowed
82
+ * between consecutive distinctive tokens. This lets "git push to main"
83
+ * match "git push origin main" (intermediate "origin") and
84
+ * "stripe charges" match "stripe charges create" (no intermediates).
85
+ *
86
+ * Returns null if the keyword has no distinctive (non-stop-word) tokens.
27
87
  */
28
- function buildPatterns(keywords) {
29
- return keywords.map((kw) => {
30
- // Escape special regex characters
31
- const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
32
- // Allow word boundary or start/end of string matching
33
- return new RegExp(escaped, "i");
34
- });
88
+ function buildKeywordRegex(keyword) {
89
+ const tokens = distinctiveTokens(keyword);
90
+ if (tokens.length === 0)
91
+ return null;
92
+ const escaped = tokens.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
93
+ // Allow up to 3 intermediate words between distinctive tokens.
94
+ // "(?:\S+\s+){0,3}" matches 0-3 non-whitespace runs followed by whitespace.
95
+ const gap = "(?:\\S+\\s+){0,3}";
96
+ const body = escaped.join("\\s+" + gap);
97
+ // Anchor with word boundaries on the outside so "stripe" doesn't match "pinstripe".
98
+ return new RegExp(`(?:^|\\W)${body}(?:$|\\W)`, "i");
35
99
  }
36
100
  /**
37
101
  * Match an action description against a rule's detection_pattern string.
@@ -41,13 +105,17 @@ function buildPatterns(keywords) {
41
105
  * @returns MatchResult
42
106
  */
43
107
  export function matchAction(action, detectionPattern) {
108
+ // Server-side stub: never match locally.
109
+ if (detectionPattern === "[server-side]") {
110
+ return { matched: false, matched_keywords: [], confidence: "low" };
111
+ }
44
112
  const keywords = parseDetectionPattern(detectionPattern);
45
- const patterns = buildPatterns(keywords);
46
- const actionLower = action.toLowerCase();
113
+ const normalizedAction = normalize(action);
47
114
  const matchedKeywords = [];
48
- for (let i = 0; i < patterns.length; i++) {
49
- if (patterns[i].test(actionLower)) {
50
- matchedKeywords.push(keywords[i]);
115
+ for (const kw of keywords) {
116
+ const re = buildKeywordRegex(kw);
117
+ if (re && re.test(normalizedAction)) {
118
+ matchedKeywords.push(kw);
51
119
  }
52
120
  }
53
121
  const matched = matchedKeywords.length > 0;
@@ -1 +1 @@
1
- {"version":3,"file":"pattern-matcher.js","sourceRoot":"","sources":["../../src/engine/pattern-matcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,gBAAwB;IAC5D,qCAAqC;IACrC,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAE9D,wCAAwC;IACxC,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAE3C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC9C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,QAAkB;IACvC,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACzB,kCAAkC;QAClC,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;QAC1D,sDAAsD;QACtD,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,MAAc,EACd,gBAAwB;IAExB,MAAM,QAAQ,GAAG,qBAAqB,CAAC,gBAAgB,CAAC,CAAC;IACzD,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAEzC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IACzC,MAAM,eAAe,GAAa,EAAE,CAAC;IAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YAClC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC;IAE3C,sDAAsD;IACtD,IAAI,UAAU,GAA8B,KAAK,CAAC;IAClD,IAAI,eAAe,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QAChC,UAAU,GAAG,MAAM,CAAC;IACtB,CAAC;SAAM,IAAI,eAAe,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACvC,UAAU,GAAG,QAAQ,CAAC;IACxB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,KAAuD;IAEvD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"pattern-matcher.js","sourceRoot":"","sources":["../../src/engine/pattern-matcher.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,19 @@
1
1
  /**
2
2
  * Rule Engine — loads rules.json, filters active rules, evaluates actions.
3
+ *
4
+ * Severity model (v1.0.1):
5
+ * - HARD = constitutional rules (always block on match, no enforcement_mode escape)
6
+ * - MEDIUM = standard rule with severity=block (warn-then-block)
7
+ * - SOFT = standard rule with severity=warn (warn only, never block)
8
+ *
9
+ * Premium rules: rules with `backend_required: true` and stub detection_pattern
10
+ * are skipped locally unless a backend URL is configured. Future versions will
11
+ * call the backend over HTTP for these.
3
12
  */
4
13
  import type { GateConfig } from "../config/defaults.js";
14
+ import { BackendClient } from "./backend-client.js";
15
+ import type { BackendRuleResult } from "../types/backend.js";
16
+ export type SeverityTier = "HARD" | "MEDIUM" | "SOFT";
5
17
  export interface Rule {
6
18
  id: string;
7
19
  name: string;
@@ -9,17 +21,66 @@ export interface Rule {
9
21
  category: string;
10
22
  enforcement: "constitutional" | "standard";
11
23
  gate_type: string;
12
- severity: "block" | "warn";
24
+ severity: "block" | "warn" | "warn-then-block";
13
25
  detection_pattern: string;
14
26
  tags: string[];
15
27
  preset_groups: string[];
28
+ backend_required?: boolean;
29
+ constitutional?: boolean;
16
30
  }
17
31
  export interface EvaluationResult {
18
32
  allowed: boolean;
19
33
  violations: Rule[];
20
34
  warnings: Rule[];
35
+ /** Highest severity tier triggered, or null if nothing matched. */
36
+ severity_tier: SeverityTier | null;
37
+ /** Rule IDs that would have blocked under enforce mode (dry-run instrumentation). */
38
+ would_have_blocked: string[];
39
+ /** Rule IDs that were skipped because backend_required=true and no backend configured. */
40
+ skipped_premium: string[];
41
+ /** Per-premium-rule backend evaluation results (when SUNAIVA_GATE_API_TOKEN is set). */
42
+ backend_results?: BackendRuleResult[];
21
43
  }
44
+ /** Test-only injection point. */
45
+ export declare function setBackendClient(client: BackendClient | null): void;
22
46
  export declare function loadAllRules(): Rule[];
47
+ /**
48
+ * Returns ONLY the rules whose `enforcement === "constitutional"`.
49
+ *
50
+ * This is the canonical "what is constitutional" function used by
51
+ * B4's immutability guard. It reads from the package-bundled rules
52
+ * (dist/rules/rules.json), never from the user-editable on-disk
53
+ * copy at ~/.sunaiva/rules.json — so a user cannot circumvent the
54
+ * guard by hand-editing their config.
55
+ *
56
+ * Cached via loadAllRules().
57
+ */
58
+ export declare function loadConstitutionalRulesOnly(): Rule[];
59
+ /**
60
+ * Resolved path that loadAllRules() reads from. Exported for tests
61
+ * and diagnostic tooling (e.g. verify-bundle / smoke-test reporting).
62
+ */
63
+ export declare function getResolvedRulesPath(): string;
23
64
  export declare function getActiveRules(config: GateConfig): Rule[];
24
65
  export declare function evaluateAction(action: string, config: GateConfig, warningCounts: Map<string, number>, context?: string): EvaluationResult;
66
+ /**
67
+ * Async wrapper around evaluateAction that ALSO delegates premium
68
+ * (`backend_required: true`) rules to the BackendClient. Used by
69
+ * `handleValidateAction` when SUNAIVA_GATE_API_TOKEN is set so premium
70
+ * rules actually get evaluated server-side instead of silently skipped.
71
+ *
72
+ * Failure mode: any backend error (network, 5xx, missing token) becomes
73
+ * a `skipped_*` status on the per-rule result. The BackendClient itself
74
+ * is fail-OPEN for the customer — a Sunaiva outage never blocks the user.
75
+ *
76
+ * Premium-rule semantics:
77
+ * - rule_id in skipped_premium → backend was NOT consulted (sync path)
78
+ * - rule_id in backend_results → backend WAS consulted; check status field
79
+ * - status === "matched" → backend wants to block; promoted to violation
80
+ * - status === "no_match" → backend evaluated clean
81
+ * - status === "skipped_*" → fail-OPEN (rule deferred, see error field)
82
+ *
83
+ * Tests #7 (token unset → skipped_no_token) lives here.
84
+ */
85
+ export declare function evaluateActionAsync(action: string, config: GateConfig, warningCounts: Map<string, number>, context?: string): Promise<EvaluationResult>;
25
86
  //# sourceMappingURL=rule-engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule-engine.d.ts","sourceRoot":"","sources":["../../src/engine/rule-engine.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAMxD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,gBAAgB,GAAG,UAAU,CAAC;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,IAAI,EAAE,CAAC;IACnB,QAAQ,EAAE,IAAI,EAAE,CAAC;CAClB;AAID,wBAAgB,YAAY,IAAI,IAAI,EAAE,CAKrC;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,CAGzD;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,MAAM,GACf,gBAAgB,CA+BlB"}
1
+ {"version":3,"file":"rule-engine.d.ts","sourceRoot":"","sources":["../../src/engine/rule-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAoB7D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,gBAAgB,GAAG,UAAU,CAAC;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,iBAAiB,CAAC;IAC/C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,IAAI,EAAE,CAAC;IACnB,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjB,mEAAmE;IACnE,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,qFAAqF;IACrF,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,0FAA0F;IAC1F,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,wFAAwF;IACxF,eAAe,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACvC;AAmBD,iCAAiC;AACjC,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAEnE;AAOD,wBAAgB,YAAY,IAAI,IAAI,EAAE,CAerC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,IAAI,IAAI,EAAE,CAMpD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,CAGzD;AAkBD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,MAAM,GACf,gBAAgB,CAkElB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,CAAC,CAqE3B"}