@sunaiva/gate 1.0.0 → 1.1.2
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/BUSINESS_LICENSE.md +70 -0
- package/CHANGELOG.md +254 -0
- package/LICENSE +0 -0
- package/README.md +451 -67
- package/README.md.bak-v1.0.0-stale-MIT +59 -0
- package/SUPPORT.md +75 -0
- package/TIER_DEFINITIONS.md +161 -0
- package/dist/config/defaults.d.ts +22 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +56 -8
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/loader.d.ts +0 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +23 -5
- package/dist/config/loader.js.map +1 -1
- package/dist/engine/backend-client.d.ts +58 -0
- package/dist/engine/backend-client.d.ts.map +1 -0
- package/dist/engine/backend-client.js +287 -0
- package/dist/engine/backend-client.js.map +1 -0
- package/dist/engine/hmac-verifier.d.ts +52 -0
- package/dist/engine/hmac-verifier.d.ts.map +1 -0
- package/dist/engine/hmac-verifier.js +159 -0
- package/dist/engine/hmac-verifier.js.map +1 -0
- package/dist/engine/immutability.d.ts +59 -0
- package/dist/engine/immutability.d.ts.map +1 -0
- package/dist/engine/immutability.js +129 -0
- package/dist/engine/immutability.js.map +1 -0
- package/dist/engine/pattern-matcher.d.ts +13 -0
- package/dist/engine/pattern-matcher.d.ts.map +1 -1
- package/dist/engine/pattern-matcher.js +85 -17
- package/dist/engine/pattern-matcher.js.map +1 -1
- package/dist/engine/rule-engine.d.ts +62 -1
- package/dist/engine/rule-engine.d.ts.map +1 -1
- package/dist/engine/rule-engine.js +224 -12
- package/dist/engine/rule-engine.js.map +1 -1
- package/dist/engine/session-state.d.ts +0 -0
- package/dist/engine/session-state.d.ts.map +1 -1
- package/dist/engine/session-state.js +8 -2
- package/dist/engine/session-state.js.map +1 -1
- package/dist/engine/ship-confidence-gate.d.ts +232 -0
- package/dist/engine/ship-confidence-gate.d.ts.map +1 -0
- package/dist/engine/ship-confidence-gate.js +768 -0
- package/dist/engine/ship-confidence-gate.js.map +1 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +293 -2
- package/dist/rules/categories.json +0 -0
- package/dist/rules/presets.json +0 -0
- package/dist/rules/rules.json +132 -64
- package/dist/tools/audit.d.ts +6 -0
- package/dist/tools/audit.d.ts.map +1 -1
- package/dist/tools/audit.js +43 -6
- package/dist/tools/audit.js.map +1 -1
- package/dist/tools/bypass.d.ts +0 -0
- package/dist/tools/bypass.d.ts.map +1 -1
- package/dist/tools/bypass.js +50 -6
- package/dist/tools/bypass.js.map +1 -1
- package/dist/tools/export-attestation.d.ts +45 -0
- package/dist/tools/export-attestation.d.ts.map +1 -0
- package/dist/tools/export-attestation.js +152 -0
- package/dist/tools/export-attestation.js.map +1 -0
- package/dist/tools/rules.d.ts +0 -0
- package/dist/tools/rules.d.ts.map +0 -0
- package/dist/tools/rules.js +0 -0
- package/dist/tools/rules.js.map +0 -0
- package/dist/tools/ship-confidence.d.ts +17 -0
- package/dist/tools/ship-confidence.d.ts.map +1 -0
- package/dist/tools/ship-confidence.js +42 -0
- package/dist/tools/ship-confidence.js.map +1 -0
- package/dist/tools/update.d.ts +0 -0
- package/dist/tools/update.d.ts.map +1 -1
- package/dist/tools/update.js +45 -9
- package/dist/tools/update.js.map +1 -1
- package/dist/tools/validate.d.ts +0 -0
- package/dist/tools/validate.d.ts.map +1 -1
- package/dist/tools/validate.js +56 -4
- package/dist/tools/validate.js.map +1 -1
- package/dist/types/backend.d.ts +69 -0
- package/dist/types/backend.d.ts.map +1 -0
- package/dist/types/backend.js +18 -0
- package/dist/types/backend.js.map +1 -0
- package/package.json +83 -65
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -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
|
|
1
|
+
{"version":3,"file":"pattern-matcher.d.ts","sourceRoot":"","sources":["../../src/engine/pattern-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAUH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;CACvC;AAuCD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBxE;AA0BD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,GACvB,WAAW,CA4Bb;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()
|
|
69
|
+
const trimmed = fragment.trim();
|
|
18
70
|
if (trimmed.length > 2) {
|
|
19
|
-
|
|
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
|
|
26
|
-
*
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
46
|
-
const actionLower = action.toLowerCase();
|
|
113
|
+
const normalizedAction = normalize(action);
|
|
47
114
|
const matchedKeywords = [];
|
|
48
|
-
for (
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
|
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;AAqB7D,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,CAIpD;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,CAiElB;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,CAmF3B"}
|