@sponsio/sdk 0.2.0-alpha.4 → 0.2.0-alpha.8
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/contracts/capability/destructive.yaml +213 -0
- package/contracts/capability/factual-claims.yaml +79 -0
- package/contracts/capability/host-config-integrity.yaml +42 -9
- package/contracts/capability/self-modify.yaml +32 -1
- package/contracts/capability/shell.yaml +19 -10
- package/contracts/incident/claude-code-secret-bypass.yaml +46 -14
- package/contracts/incident/cursor-railway-wipe.yaml +60 -4
- package/contracts/incident/openclaw.yaml +9 -63
- package/dist/cli/cli.js +32 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/packs.d.ts.map +1 -1
- package/dist/cli/packs.js +13 -7
- package/dist/cli/packs.js.map +1 -1
- package/dist/cloud/client.d.ts +76 -0
- package/dist/cloud/client.d.ts.map +1 -0
- package/dist/cloud/client.js +198 -0
- package/dist/cloud/client.js.map +1 -0
- package/dist/cloud/evidence.d.ts +108 -0
- package/dist/cloud/evidence.d.ts.map +1 -0
- package/dist/cloud/evidence.js +184 -0
- package/dist/cloud/evidence.js.map +1 -0
- package/dist/cloud/privacy.d.ts +67 -0
- package/dist/cloud/privacy.d.ts.map +1 -0
- package/dist/cloud/privacy.js +170 -0
- package/dist/cloud/privacy.js.map +1 -0
- package/dist/cloud/view-model.d.ts +49 -0
- package/dist/cloud/view-model.d.ts.map +1 -0
- package/dist/cloud/view-model.js +172 -0
- package/dist/cloud/view-model.js.map +1 -0
- package/dist/core/config-loader.js +16 -0
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/evaluator.d.ts +2 -0
- package/dist/core/evaluator.d.ts.map +1 -1
- package/dist/core/evaluator.js +90 -4
- package/dist/core/evaluator.js.map +1 -1
- package/dist/core/grounding.d.ts +43 -7
- package/dist/core/grounding.d.ts.map +1 -1
- package/dist/core/grounding.js +180 -25
- package/dist/core/grounding.js.map +1 -1
- package/dist/core/nl-parser.d.ts +1 -1
- package/dist/core/nl-parser.d.ts.map +1 -1
- package/dist/core/nl-parser.js +616 -40
- package/dist/core/nl-parser.js.map +1 -1
- package/dist/core/pattern-factory.d.ts.map +1 -1
- package/dist/core/pattern-factory.js +7 -1
- package/dist/core/pattern-factory.js.map +1 -1
- package/dist/core/patterns.d.ts +69 -0
- package/dist/core/patterns.d.ts.map +1 -1
- package/dist/core/patterns.js +133 -10
- package/dist/core/patterns.js.map +1 -1
- package/dist/core/tool-names.d.ts +59 -0
- package/dist/core/tool-names.d.ts.map +1 -0
- package/dist/core/tool-names.js +128 -0
- package/dist/core/tool-names.js.map +1 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +367 -32
- package/dist/index.js.map +1 -1
- package/dist/integrations/claude-agent.d.ts +5 -0
- package/dist/integrations/claude-agent.d.ts.map +1 -1
- package/dist/integrations/claude-agent.js +42 -13
- package/dist/integrations/claude-agent.js.map +1 -1
- package/dist/integrations/google-adk.d.ts +5 -3
- package/dist/integrations/google-adk.d.ts.map +1 -1
- package/dist/integrations/google-adk.js +9 -5
- package/dist/integrations/google-adk.js.map +1 -1
- package/dist/integrations/langchain.d.ts +7 -0
- package/dist/integrations/langchain.d.ts.map +1 -1
- package/dist/integrations/langchain.js +21 -3
- package/dist/integrations/langchain.js.map +1 -1
- package/dist/integrations/openai-agents.d.ts +22 -10
- package/dist/integrations/openai-agents.d.ts.map +1 -1
- package/dist/integrations/openai-agents.js +51 -12
- package/dist/integrations/openai-agents.js.map +1 -1
- package/dist/integrations/openai.d.ts +11 -0
- package/dist/integrations/openai.d.ts.map +1 -1
- package/dist/integrations/openai.js +130 -48
- package/dist/integrations/openai.js.map +1 -1
- package/dist/integrations/vercel-ai.d.ts +20 -9
- package/dist/integrations/vercel-ai.d.ts.map +1 -1
- package/dist/integrations/vercel-ai.js +131 -50
- package/dist/integrations/vercel-ai.js.map +1 -1
- package/package.json +12 -8
- package/prompts/onboard.md +17 -4
- package/contracts/core/llm_safety.yaml +0 -138
package/dist/core/nl-parser.js
CHANGED
|
@@ -7,71 +7,466 @@
|
|
|
7
7
|
* "tool `X` at most 3 times"
|
|
8
8
|
* "tools `A` and `B` are mutually exclusive"
|
|
9
9
|
*/
|
|
10
|
-
import { mustPrecede, alwaysFollowedBy, rateLimit, idempotent, mutualExclusion, noReversal, cooldown, deadline, maxLength, noPii, noKeywords, } from "./patterns.js";
|
|
10
|
+
import { mustPrecede, alwaysFollowedBy, rateLimit, idempotent, mutualExclusion, noReversal, argAllowlist, argBlacklist, approvalFreshness, auditAfter, backupBeforeDestructive, boundedRetry, cooldown, dataIntact, deadline, dryRunBeforeCommit, duplicateCallLimit, maxLength, mustConfirm, noDataLeak, noPii, noKeywords, sanitizedBeforeSink, scopeLimit, requiresPermission, segregationOfDuty, } from "./patterns.js";
|
|
11
11
|
import { Atom } from "./formula.js";
|
|
12
|
-
|
|
12
|
+
// Bare snake_case identifiers. At least one underscore, so ordinary
|
|
13
|
+
// English words are not read as tool names.
|
|
14
|
+
const BARE_SNAKE = /\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\b/g;
|
|
15
|
+
// Phrases that look snake_case when squished and are not tools.
|
|
16
|
+
const BARE_STOP = new Set([
|
|
17
|
+
"at_most", "at_least", "no_more", "per_session", "per_call",
|
|
18
|
+
"must_not", "should_not", "same_session", "each_other",
|
|
19
|
+
]);
|
|
20
|
+
const CUE_AND = /(?:^|\s)(?:tool|tools|call(?:ing)?|run(?:ning)?|execute|invoke)\s+([a-zA-Z][a-zA-Z0-9_]*)\s+and\s+([a-zA-Z][a-zA-Z0-9_]*)/i;
|
|
21
|
+
const CUE_PHRASE = /(?:^|\s)(?:tool|tools|action|actions|call(?:ing)?|run(?:ning)?|execute|invoke|use|using)\s+([a-zA-Z][a-zA-Z0-9_]*)/gi;
|
|
22
|
+
const TOOL_BEFORE_FIELD = /([a-zA-Z][a-zA-Z0-9_]*)\s+(?:command|args?|arguments?|input|params?)/i;
|
|
23
|
+
// English words that must never be read as a tool name.
|
|
24
|
+
const STOP_WORDS = new Set(["a", "admin", "after", "agent", "all", "allowed", "also", "always", "an", "and", "any", "are", "as", "at", "be", "been", "before", "being", "between", "both", "but", "by", "called", "can", "confirmed", "could", "data", "did", "different", "do", "does", "each", "every", "few", "file", "first", "for", "from", "get", "got", "had", "has", "have", "he", "her", "him", "if", "in", "is", "it", "its", "just", "last", "let", "made", "make", "may", "me", "more", "most", "must", "my", "never", "new", "no", "nor", "not", "of", "old", "on", "once", "only", "or", "other", "our", "permission", "required", "restricted", "same", "set", "shall", "she", "should", "so", "some", "than", "that", "the", "them", "then", "these", "they", "this", "those", "to", "too", "us", "user", "very", "was", "we", "were", "will", "with", "within", "would", "you", "your"]);
|
|
25
|
+
/**
|
|
26
|
+
* The tool names a sentence mentions. Parity with Python's
|
|
27
|
+
* ``_extract_actions``, and the reason this side used to drop rules
|
|
28
|
+
* Python accepted: it read backticks and nothing else, so
|
|
29
|
+
* "check_policy must precede issue_refund" named no tools here and
|
|
30
|
+
* parsed to null while Python armed it.
|
|
31
|
+
*
|
|
32
|
+
* One method wins, in this order: backticks, then quotes, then bare
|
|
33
|
+
* snake_case, then the cue phrases. Mixing them would let a sentence
|
|
34
|
+
* that quotes one tool pick up an English word as the other.
|
|
35
|
+
*/
|
|
13
36
|
function extractTools(text) {
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
return
|
|
17
|
-
|
|
37
|
+
const ticked = [...text.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
|
|
38
|
+
if (ticked.length)
|
|
39
|
+
return ticked;
|
|
40
|
+
const quoted = [...text.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
|
|
41
|
+
if (quoted.length)
|
|
42
|
+
return quoted;
|
|
43
|
+
const bare = [...text.matchAll(BARE_SNAKE)]
|
|
44
|
+
.map((m) => m[1])
|
|
45
|
+
.filter((w) => !BARE_STOP.has(w));
|
|
46
|
+
if (bare.length)
|
|
47
|
+
return bare;
|
|
48
|
+
const pair = text.match(CUE_AND);
|
|
49
|
+
if (pair) {
|
|
50
|
+
const both = [pair[1], pair[2]].filter((w) => !STOP_WORDS.has(w.toLowerCase()));
|
|
51
|
+
if (both.length >= 2)
|
|
52
|
+
return both;
|
|
53
|
+
}
|
|
54
|
+
const cued = [...text.matchAll(CUE_PHRASE)]
|
|
55
|
+
.map((m) => m[1])
|
|
56
|
+
.filter((w) => !STOP_WORDS.has(w.toLowerCase()));
|
|
57
|
+
if (cued.length)
|
|
58
|
+
return cued;
|
|
59
|
+
const beforeField = text.match(TOOL_BEFORE_FIELD);
|
|
60
|
+
if (beforeField && !STOP_WORDS.has(beforeField[1].toLowerCase())) {
|
|
61
|
+
return [beforeField[1]];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
18
64
|
}
|
|
19
|
-
|
|
20
|
-
|
|
65
|
+
// Small numbers people write as words. Parity with Python's
|
|
66
|
+
// ``_WORD_NUMBERS``: "at most once" has to mean 1, or the rule is
|
|
67
|
+
// dropped rather than armed.
|
|
68
|
+
const WORD_NUMBERS = {
|
|
69
|
+
one: 1, once: 1, two: 2, twice: 2, three: 3, four: 4, five: 5,
|
|
70
|
+
six: 6, seven: 7, eight: 8, nine: 9, ten: 10,
|
|
71
|
+
};
|
|
72
|
+
/** A digit, or a small number spelled out. Parity with ``_parse_number``. */
|
|
73
|
+
function parseNumber(text) {
|
|
21
74
|
const m = text.match(/(\d+)/);
|
|
75
|
+
if (m)
|
|
76
|
+
return parseInt(m[1], 10);
|
|
77
|
+
const lower = text.toLowerCase();
|
|
78
|
+
for (const [word, n] of Object.entries(WORD_NUMBERS)) {
|
|
79
|
+
if (lower.includes(word))
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/** Parity with ``_parse_rate_limit_count``, including the word forms. */
|
|
85
|
+
function parseRateLimitCount(text) {
|
|
86
|
+
const lower = text.toLowerCase();
|
|
87
|
+
let m = lower.match(/at most (\d+)/) ??
|
|
88
|
+
lower.match(/(\d+)\s*(?:times|invocations|calls|per)/) ??
|
|
89
|
+
lower.match(/limit.*?(\d+)/) ??
|
|
90
|
+
lower.match(/(?:no more than|up to|maximum|max|more than)\s+(\d+)/);
|
|
91
|
+
if (m)
|
|
92
|
+
return parseInt(m[1], 10);
|
|
93
|
+
const words = Object.keys(WORD_NUMBERS).join("|");
|
|
94
|
+
m = lower.match(new RegExp(`(?:more than|at most|no more than|up to)\\s+(${words})\\b`));
|
|
95
|
+
if (m)
|
|
96
|
+
return WORD_NUMBERS[m[1]];
|
|
97
|
+
for (const [word, n] of Object.entries(WORD_NUMBERS)) {
|
|
98
|
+
if (lower.includes(word) && new RegExp(`${word}\\s+(?:times|calls|per)`).test(lower)) {
|
|
99
|
+
return n;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/** Parity with ``_parse_retry_count``. */
|
|
105
|
+
function parseRetryCount(text) {
|
|
106
|
+
const lower = text.toLowerCase();
|
|
107
|
+
const m = lower.match(/at most (\d+)\s*retr/) ??
|
|
108
|
+
lower.match(/(\d+)\s*retr/) ??
|
|
109
|
+
lower.match(/max(?:imum)?\s*(\d+)/);
|
|
22
110
|
return m ? parseInt(m[1], 10) : null;
|
|
23
111
|
}
|
|
112
|
+
/** Parity with ``_parse_step_count``. */
|
|
113
|
+
function parseStepCount(text) {
|
|
114
|
+
const lower = text.toLowerCase();
|
|
115
|
+
const m = lower.match(/(\d+)\s*steps?/) ?? lower.match(/cooldown\s+(?:of\s+)?(\d+)/);
|
|
116
|
+
return m ? parseInt(m[1], 10) : null;
|
|
117
|
+
}
|
|
118
|
+
const BACKTICKED = /`([^`]+)`/g;
|
|
119
|
+
const QUOTED = /["']([^"']+)["']/g;
|
|
120
|
+
function allMatches(re, text) {
|
|
121
|
+
return [...text.matchAll(new RegExp(re.source, "g"))].map((m) => m[1]);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The shapes a sentence forbids. Parity with
|
|
125
|
+
* ``_extract_blacklist_patterns``: read the tail after the verb, prefer
|
|
126
|
+
* whatever is quoted there, and otherwise split the tail on "or" / ","
|
|
127
|
+
* / "and" so a bare "must not contain rm -rf or sudo" still bans two
|
|
128
|
+
* things rather than one long string.
|
|
129
|
+
*/
|
|
130
|
+
function extractDelimitedTail(text) {
|
|
131
|
+
const m = text.match(/(?:contain|include|allow|permit)\s+(.+)/i);
|
|
132
|
+
if (!m)
|
|
133
|
+
return [];
|
|
134
|
+
const tail = m[1];
|
|
135
|
+
const items = allMatches(BACKTICKED, tail);
|
|
136
|
+
if (items.length)
|
|
137
|
+
return items;
|
|
138
|
+
const quoted = allMatches(QUOTED, tail);
|
|
139
|
+
if (quoted.length)
|
|
140
|
+
return quoted;
|
|
141
|
+
return tail
|
|
142
|
+
.split(/\s+or\s+|\s*,\s*|\s+and\s+/)
|
|
143
|
+
.map((x) => x.trim().replace(/\.$/, ""))
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
/** Parity with ``_extract_allowlist_patterns``. */
|
|
147
|
+
function extractAllowlistPatterns(text) {
|
|
148
|
+
const m = text.match(/(?:one\s+of|in|allow(?:list)?|whitelist|permit)\s+(.+)/i);
|
|
149
|
+
if (!m)
|
|
150
|
+
return [];
|
|
151
|
+
const tail = m[1];
|
|
152
|
+
const items = allMatches(BACKTICKED, tail);
|
|
153
|
+
if (items.length)
|
|
154
|
+
return items;
|
|
155
|
+
const quoted = allMatches(QUOTED, tail);
|
|
156
|
+
if (quoted.length)
|
|
157
|
+
return quoted;
|
|
158
|
+
return tail
|
|
159
|
+
.split(/\s+or\s+|\s*,\s*|\s+and\s+/)
|
|
160
|
+
.map((x) => x.trim().replace(/\.$/, ""))
|
|
161
|
+
.filter(Boolean);
|
|
162
|
+
}
|
|
163
|
+
// Generated from Python's ``_KEYWORD_RULES`` by scripts/gen_ts_rules.py,
|
|
164
|
+
// because a table of 155 regexes copied by hand drifts, and it drifted:
|
|
165
|
+
// this side carried 46 of them and silently returned null for the rest,
|
|
166
|
+
// so a rulebook that armed 23 patterns in Python armed 12 here and said
|
|
167
|
+
// nothing about the other 11.
|
|
168
|
+
//
|
|
169
|
+
// ``minArgs`` is this side's own: Python has no pre-gate and lets each
|
|
170
|
+
// branch check its own arity, so the numbers here are what each handler
|
|
171
|
+
// below actually needs, not the ones in Python's table.
|
|
24
172
|
const KEYWORD_RULES = [
|
|
25
|
-
// Deadline (before rate_limit so "within N steps" doesn't get swallowed by
|
|
26
|
-
// any "N times"-style rule). Mirrors the Python ``dsl_to_contract`` regex set.
|
|
27
173
|
{
|
|
28
174
|
patterns: [
|
|
29
|
-
/
|
|
30
|
-
/
|
|
31
|
-
/
|
|
175
|
+
/arg(?:ument)?s?\s+(?:must\s+be|must\s+match)\s+(?:one\s+of|in)/,
|
|
176
|
+
/(?:command|input|param|recipient|to|host|domain|url)\s+must\s+be\s+(?:one\s+of|in)/,
|
|
177
|
+
/allowlist/,
|
|
178
|
+
/whitelist/,
|
|
179
|
+
/only\s+(?:allow|permit)\s+(?:the\s+)?(?:value|values|recipient|recipients|host|hosts|domain|domains)/,
|
|
180
|
+
/restrict\s+(?:.*\s+)?(?:to|in)\s+(?:the\s+)?(?:allowed|allow-listed|whitelisted)\s+(?:value|values|set|list)/,
|
|
32
181
|
],
|
|
33
|
-
patternName: "
|
|
182
|
+
patternName: "arg_allowlist",
|
|
183
|
+
minArgs: 1,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
patterns: [
|
|
187
|
+
/arg(?:ument)?s?\s+(?:must\s+)?not\s+contain/,
|
|
188
|
+
/\b(?:arg|argument|field|param(?:eter)?)s?\s+[`"'][^`"']+[`"']\s+(?:must\s+)?not\s+contain/,
|
|
189
|
+
/(?:command|input|param|query|body|text|content|url|path)\s+must\s+not\s+contain/,
|
|
190
|
+
/blacklist/,
|
|
191
|
+
/must\s+not\s+contain\s+(?:.*(?:rm\s*-rf|sudo|DROP|eval))/,
|
|
192
|
+
/forbid.*(?:in\s+(?:arguments?|params?|input))/,
|
|
193
|
+
/ban\s+(?:patterns?|commands?)\s+in/,
|
|
194
|
+
],
|
|
195
|
+
patternName: "arg_blacklist",
|
|
196
|
+
minArgs: 1,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
patterns: [
|
|
200
|
+
/restrict\s+(?:file\s+)?(?:access|operations?)\s+to\s+(?:`|\/)/,
|
|
201
|
+
/scope\s*limit/,
|
|
202
|
+
/only\s+(?:access|read|write|operate)\s+(?:files?\s+)?(?:in|within|under)\s+(?:`|\/)/,
|
|
203
|
+
/file\s+(?:operations?\s+)?restricted\s+to\s+(?:`|\/)/,
|
|
204
|
+
/(?:paths?|files?|directories?)\s+(?:must\s+be\s+)?(?:within|under)\s+(?:`|\/)/,
|
|
205
|
+
/confine.*to\s+(?:\/|`)/,
|
|
206
|
+
/restricted\s+to\s+(?:`?\/)/,
|
|
207
|
+
],
|
|
208
|
+
patternName: "scope_limit",
|
|
209
|
+
minArgs: 0,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
patterns: [
|
|
213
|
+
/data\s+(?:must\s+)?remain\s+(?:un(?:modified|changed|altered)|intact)/,
|
|
214
|
+
/(?:must\s+)?(?:only|exclusively)\s+(?:read|operate\s+on)\s+(?:from\s+)?(?:original|unmodified)/,
|
|
215
|
+
/data\s*intact/,
|
|
216
|
+
/read[- ]?only\s+(?:from|on)\b/,
|
|
217
|
+
],
|
|
218
|
+
patternName: "data_intact",
|
|
219
|
+
minArgs: 1,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
patterns: [
|
|
223
|
+
/dry[- ]?run.*before/,
|
|
224
|
+
/plan.*before.*(?:apply|commit|deploy|execute)/,
|
|
225
|
+
/(?:apply|commit|deploy|execute).*requires?.*dry[- ]?run/,
|
|
226
|
+
],
|
|
227
|
+
patternName: "dry_run_before_commit",
|
|
228
|
+
minArgs: 2,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
patterns: [
|
|
232
|
+
/backup.*before/,
|
|
233
|
+
/snapshot.*before/,
|
|
234
|
+
/(?:delete|drop|destroy|destructive).*requires?.*(?:backup|snapshot)/,
|
|
235
|
+
],
|
|
236
|
+
patternName: "backup_before_destructive",
|
|
237
|
+
minArgs: 2,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
patterns: [
|
|
241
|
+
/audit.*after/,
|
|
242
|
+
/log.*after/,
|
|
243
|
+
/(?:must|should).*be\s+(?:audited|logged)/,
|
|
244
|
+
/(?:audit|log)\s+(?:required|needed)/,
|
|
245
|
+
],
|
|
246
|
+
patternName: "audit_after",
|
|
247
|
+
minArgs: 1,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
patterns: [
|
|
251
|
+
/fresh\s+approval/,
|
|
252
|
+
/approval.*(?:within|expires?|expire)/,
|
|
253
|
+
/(?:approval|authorization).*fresh/,
|
|
254
|
+
/(?:approve|approval).*(?:\d+)\s+steps?/,
|
|
255
|
+
],
|
|
256
|
+
patternName: "approval_freshness",
|
|
257
|
+
minArgs: 1,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
patterns: [
|
|
261
|
+
/sanitize.*before/,
|
|
262
|
+
/saniti[sz]ed.*before/,
|
|
263
|
+
/(?:untrusted|external|web|email).*sanitize.*(?:before|then)/,
|
|
264
|
+
/(?:source|input).*sanitizer.*sink/,
|
|
265
|
+
],
|
|
266
|
+
patternName: "sanitized_before_sink",
|
|
267
|
+
minArgs: 3,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
patterns: [
|
|
271
|
+
/duplicate\s+call/,
|
|
272
|
+
/same.*request.*at most/,
|
|
273
|
+
/same\s+(?:tool|api|request|args?).*at most/,
|
|
274
|
+
/repeat(?:ed)?\s+(?:same\s+)?(?:call|request)/,
|
|
275
|
+
/no duplicate/,
|
|
276
|
+
/never repeat/,
|
|
277
|
+
],
|
|
278
|
+
patternName: "duplicate_call_limit",
|
|
34
279
|
minArgs: 2,
|
|
35
280
|
},
|
|
36
|
-
// Rate limit (before idempotent — "at most N times")
|
|
37
281
|
{
|
|
38
|
-
patterns: [
|
|
282
|
+
patterns: [
|
|
283
|
+
/at most.*retr/,
|
|
284
|
+
/retry.*at most/,
|
|
285
|
+
/bounded retry/,
|
|
286
|
+
/max.*retries/,
|
|
287
|
+
/maximum.*retries/,
|
|
288
|
+
/no more than.*retr/,
|
|
289
|
+
/limit.*retries?\s+to\b/,
|
|
290
|
+
],
|
|
291
|
+
patternName: "bounded_retry",
|
|
292
|
+
minArgs: 1,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
patterns: [
|
|
296
|
+
/rate\s*limit/,
|
|
297
|
+
/at most.*times/,
|
|
298
|
+
/maximum.*invocations/,
|
|
299
|
+
/limit.*(?:calls|invocations|uses)\b/,
|
|
300
|
+
/must not be called more than/,
|
|
301
|
+
/(?:at most|no more than|up to|maximum|max)\s+(\d+)\s+(?:per|times|calls)/,
|
|
302
|
+
/limit.*to\s+(\d+)\s+(?:per|times|calls)/,
|
|
303
|
+
],
|
|
39
304
|
patternName: "rate_limit",
|
|
40
305
|
minArgs: 1,
|
|
41
306
|
},
|
|
42
|
-
// Idempotent
|
|
43
307
|
{
|
|
44
|
-
patterns: [
|
|
308
|
+
patterns: [
|
|
309
|
+
/idempotent/,
|
|
310
|
+
/at most once/,
|
|
311
|
+
/only (?:once|run once|call(?:ed)? once)/,
|
|
312
|
+
/called? once\b/,
|
|
313
|
+
/should only (?:run|be called|execute) once/,
|
|
314
|
+
/single invocation/,
|
|
315
|
+
/no repeated calls?\b/,
|
|
316
|
+
],
|
|
45
317
|
patternName: "idempotent",
|
|
46
318
|
minArgs: 1,
|
|
47
319
|
},
|
|
48
|
-
// Mutual exclusion
|
|
49
320
|
{
|
|
50
|
-
patterns: [
|
|
321
|
+
patterns: [
|
|
322
|
+
/mutually exclusive/,
|
|
323
|
+
/exactly one of/,
|
|
324
|
+
/either.*or.*not both/,
|
|
325
|
+
/cannot (?:both|call both)/,
|
|
326
|
+
/only one of/,
|
|
327
|
+
/at most one of/,
|
|
328
|
+
],
|
|
51
329
|
patternName: "mutual_exclusion",
|
|
52
330
|
minArgs: 2,
|
|
53
331
|
},
|
|
54
|
-
// Always followed by
|
|
55
332
|
{
|
|
56
|
-
patterns: [
|
|
57
|
-
|
|
333
|
+
patterns: [
|
|
334
|
+
/never together/,
|
|
335
|
+
/never\s+(?:call(?:ing)?\s+)?[^,]*\band\b[^,]*\btogether\b/,
|
|
336
|
+
/never both/,
|
|
337
|
+
/not at the same time/,
|
|
338
|
+
/never co-occur/,
|
|
339
|
+
/must never.*called together/,
|
|
340
|
+
/never be called together/,
|
|
341
|
+
/not.*(?:in|within|during)\s+(?:the\s+)?same\s+session/,
|
|
342
|
+
/do not call.*(?:and|,).*(?:in|within|during)\s+(?:the\s+)?same/,
|
|
343
|
+
],
|
|
344
|
+
patternName: "mutual_exclusion",
|
|
58
345
|
minArgs: 2,
|
|
59
346
|
},
|
|
60
|
-
// No reversal
|
|
61
347
|
{
|
|
62
|
-
patterns: [
|
|
63
|
-
|
|
348
|
+
patterns: [
|
|
349
|
+
/cooldown/,
|
|
350
|
+
/cool\s*down\s+(?:of|between|period\s+of)\s+\d+/,
|
|
351
|
+
/minimum\s+\d+\s+steps?\s+between/,
|
|
352
|
+
/at least\s+\d+\s+steps?\s+between/,
|
|
353
|
+
/wait\s+\d+\s+steps?\s+between/,
|
|
354
|
+
/gap of\s+\d+\s+steps?/,
|
|
355
|
+
/interval\s+(?:of\s+)?\d+\s+steps?/,
|
|
356
|
+
],
|
|
357
|
+
patternName: "cooldown",
|
|
358
|
+
minArgs: 1,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
patterns: [
|
|
362
|
+
/segregation of dut/,
|
|
363
|
+
/separation of dut/,
|
|
364
|
+
/same agent.*cannot.*both/,
|
|
365
|
+
/different agent/,
|
|
366
|
+
/cannot do both/,
|
|
367
|
+
/must be (?:done|performed) by different/,
|
|
368
|
+
/two[- ]person\s+rule/,
|
|
369
|
+
/dual\s+control/,
|
|
370
|
+
],
|
|
371
|
+
patternName: "segregation_of_duty",
|
|
64
372
|
minArgs: 2,
|
|
65
373
|
},
|
|
66
|
-
// Cooldown
|
|
67
374
|
{
|
|
68
|
-
patterns: [
|
|
69
|
-
|
|
375
|
+
patterns: [
|
|
376
|
+
/within\s+\d+\s+steps?\s+(?:of|after)/,
|
|
377
|
+
/deadline\s+(?:of\s+)?\d+\s+steps?/,
|
|
378
|
+
/must.*within\s+\d+\s+steps?/,
|
|
379
|
+
/at most\s+\d+\s+steps?\s+after/,
|
|
380
|
+
],
|
|
381
|
+
patternName: "deadline",
|
|
382
|
+
minArgs: 2,
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
patterns: [
|
|
386
|
+
/must be confirmed/,
|
|
387
|
+
/confirm(?:ation)?\s+(?:before|required|needed)/,
|
|
388
|
+
/requires?\s+confirmation/,
|
|
389
|
+
/must confirm before/,
|
|
390
|
+
/(?:needs?|requires?)\s+(?:user\s+)?(?:approval|consent)\s+before/,
|
|
391
|
+
/without\s+confirmation/,
|
|
392
|
+
/never\s+call.*without\s+confirm/,
|
|
393
|
+
],
|
|
394
|
+
patternName: "must_confirm",
|
|
70
395
|
minArgs: 1,
|
|
71
396
|
},
|
|
72
|
-
// Must precede (last — most general, requires backtick context)
|
|
73
397
|
{
|
|
74
|
-
patterns: [
|
|
398
|
+
patterns: [
|
|
399
|
+
/cannot.*after\s+approv/,
|
|
400
|
+
/no reversal/,
|
|
401
|
+
/never\s+reverse/,
|
|
402
|
+
/cannot\s+deny\s+after/,
|
|
403
|
+
/cannot\s+reject\s+after/,
|
|
404
|
+
/cannot\s+contradict/,
|
|
405
|
+
/cannot\s+be\s+reversed/,
|
|
406
|
+
/(?:never|cannot|must\s+not|don'?t|do\s+not)\s+(?:call\s+)?.*after\s+(?:calling\s+)?/,
|
|
407
|
+
/^\s*after\s+.*\b(?:never|cannot|must\s+not|don'?t|do\s+not)\b/,
|
|
408
|
+
/forbidden\s+after/,
|
|
409
|
+
/prohibited\s+after/,
|
|
410
|
+
/must\s+not\s+follow/,
|
|
411
|
+
/must\s+not.*after/,
|
|
412
|
+
/should\s+not\s+follow/,
|
|
413
|
+
/not\s+allowed\s+after/,
|
|
414
|
+
/once.*(?:cannot|must\s+not|never)/,
|
|
415
|
+
/irreversible/,
|
|
416
|
+
],
|
|
417
|
+
patternName: "no_reversal",
|
|
418
|
+
minArgs: 2,
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
patterns: [
|
|
422
|
+
/requires?\s+(?:\w+\s+)?permission/,
|
|
423
|
+
/needs?\s+permission/,
|
|
424
|
+
/must\s+have\s+permission/,
|
|
425
|
+
/(?:requires?|needs?)\s+(?:\w+\s+)?(?:authorization|auth)\b/,
|
|
426
|
+
/requires?\s+admin\b/,
|
|
427
|
+
/(?:only\s+)?(?:authorized|permitted)\s+(?:users?|agents?|roles?)\s+(?:can|may)/,
|
|
428
|
+
/require\s+\w+\s+(?:permission|role|access)\s+to\b/,
|
|
429
|
+
],
|
|
430
|
+
patternName: "requires_permission",
|
|
431
|
+
minArgs: 2,
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
patterns: [
|
|
435
|
+
/no data leak/,
|
|
436
|
+
/data must not (?:flow|leak|be\s+sent)/,
|
|
437
|
+
/no leak/,
|
|
438
|
+
/(?:must\s+)?not\s+(?:send|transmit|expose|share).*(?:to\s+external|outside)/,
|
|
439
|
+
/protect.*from\s+(?:leaking|exposure)/,
|
|
440
|
+
],
|
|
441
|
+
patternName: "no_data_leak",
|
|
442
|
+
minArgs: 2,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
patterns: [
|
|
446
|
+
/(?:must\s+be\s+|always\s+)?followed\s+by/,
|
|
447
|
+
/must eventually follow/,
|
|
448
|
+
/eventually.*after/,
|
|
449
|
+
/after\s+(?:calling\s+)?.*(?:always|must)\s+(?:call\s+|run\s+)?/,
|
|
450
|
+
/(?:always|must)\s+(?:call|run|execute)\s+.*after/,
|
|
451
|
+
/whenever.*(?:is\s+)?called.*(?:must|should|always)/,
|
|
452
|
+
/(?:should|must)\s+(?:always\s+)?come\s+after/,
|
|
453
|
+
],
|
|
454
|
+
patternName: "always_followed_by",
|
|
455
|
+
minArgs: 2,
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
patterns: [
|
|
459
|
+
/precede/,
|
|
460
|
+
/prior to\s+`/,
|
|
461
|
+
/`[^`]+`\s+(?:must\s+)?(?:be\s+)?(?:called\s+|run\s+|executed\s+)?before\s+`/,
|
|
462
|
+
/before\s+(?:calling\s+)?`/,
|
|
463
|
+
/required\s+before\s+`/,
|
|
464
|
+
/is\s+(?:a\s+)?prerequisite\s+for\b/,
|
|
465
|
+
/(?:always|must)\s+run\s+`[^`]+`\s+first/,
|
|
466
|
+
/is\s+required\s+before/,
|
|
467
|
+
/needs?\s+to\s+(?:be\s+)?(?:called|run)\s+before/,
|
|
468
|
+
/must\s+(?:be\s+)?(?:called|run|executed)\s+first/,
|
|
469
|
+
],
|
|
75
470
|
patternName: "must_precede",
|
|
76
471
|
minArgs: 2,
|
|
77
472
|
},
|
|
@@ -94,7 +489,11 @@ function parseBareCalledAtom(text, tools) {
|
|
|
94
489
|
return {
|
|
95
490
|
formula: new Atom("called", [tools[0]]),
|
|
96
491
|
desc: `called(${tools[0]})`,
|
|
97
|
-
patternName: "
|
|
492
|
+
patternName: "trigger_called",
|
|
493
|
+
// Empty, as Python leaves it. The tool is in the atom; putting it
|
|
494
|
+
// here too would make the same rule group differently on the two
|
|
495
|
+
// sides, which is the one thing args exists to prevent.
|
|
496
|
+
args: [],
|
|
98
497
|
liveness: false,
|
|
99
498
|
};
|
|
100
499
|
}
|
|
@@ -166,7 +565,44 @@ function tryResponseContent(text) {
|
|
|
166
565
|
}
|
|
167
566
|
return null;
|
|
168
567
|
}
|
|
169
|
-
|
|
568
|
+
const NEGATION_RE = /\b(?:never|cannot|can\s+not|must\s+not|don'?t|do\s+not|no)\b/;
|
|
569
|
+
/**
|
|
570
|
+
* True for "never call A after B", where B is the commitment.
|
|
571
|
+
*
|
|
572
|
+
* Decided by position rather than by phrase: the phrase list this
|
|
573
|
+
* supplements matches "never after" and not "never call `x` after", and
|
|
574
|
+
* every new way of saying it would need another entry. Parity with
|
|
575
|
+
* Python's ``_forbidden_action_comes_first``.
|
|
576
|
+
*/
|
|
577
|
+
function forbiddenActionComesFirst(lower, first, second) {
|
|
578
|
+
const iFirst = lower.indexOf(first.toLowerCase());
|
|
579
|
+
if (iFirst < 0)
|
|
580
|
+
return false;
|
|
581
|
+
const iSecond = lower.indexOf(second.toLowerCase(), iFirst + 1);
|
|
582
|
+
if (iSecond < 0 || iSecond <= iFirst)
|
|
583
|
+
return false;
|
|
584
|
+
// "after" has to sit between the two actions: in "after `b`, never call
|
|
585
|
+
// `a`" the commitment already comes first and must not be swapped.
|
|
586
|
+
if (!/\bafter\b/.test(lower.slice(iFirst + first.length, iSecond)))
|
|
587
|
+
return false;
|
|
588
|
+
return NEGATION_RE.test(lower.slice(0, iFirst));
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* The longest rule sentence this will look at, matching Python's
|
|
592
|
+
* ``_MAX_NL_LINE_LEN``.
|
|
593
|
+
*
|
|
594
|
+
* Several of the regexes below are quadratic on adversarial input: a
|
|
595
|
+
* long run of digits before ``\s*steps?`` makes the engine retry from
|
|
596
|
+
* every position. Python has always truncated and this side never did,
|
|
597
|
+
* so the same rulebook that cost Python milliseconds could hang a
|
|
598
|
+
* TypeScript agent — and on a platform whose customers supply their own
|
|
599
|
+
* overlay rules, that string is not fully trusted.
|
|
600
|
+
*
|
|
601
|
+
* A sentence longer than this is not a rule anybody wrote.
|
|
602
|
+
*/
|
|
603
|
+
const MAX_RULE_CHARS = 10_000;
|
|
604
|
+
export function parseNl(input) {
|
|
605
|
+
const text = input.length > MAX_RULE_CHARS ? input.slice(0, MAX_RULE_CHARS) : input;
|
|
170
606
|
// P2 response-content patterns first — keep them ahead of the
|
|
171
607
|
// generic keyword cascade so "response must not contain emails"
|
|
172
608
|
// doesn't get swallowed by something more general.
|
|
@@ -184,10 +620,19 @@ export function parseNl(text) {
|
|
|
184
620
|
switch (rule.patternName) {
|
|
185
621
|
case "must_precede":
|
|
186
622
|
return mustPrecede(tools[0], tools[1]);
|
|
187
|
-
case "always_followed_by":
|
|
188
|
-
|
|
623
|
+
case "always_followed_by": {
|
|
624
|
+
// English puts the trigger second in two shapes, and reading the
|
|
625
|
+
// names in the order they appear built the rule backwards:
|
|
626
|
+
// "`b` should come after `a`" and "always call `b` after `a`"
|
|
627
|
+
// both mean a triggers b. Parity with Python.
|
|
628
|
+
const swap = /come\s+after|should\s+(?:always\s+)?(?:come|happen)\s+after/.test(lower) ||
|
|
629
|
+
/(?:always|must)\s+(?:call|run)\s+\S+\s+after\b/.test(lower);
|
|
630
|
+
return swap
|
|
631
|
+
? alwaysFollowedBy(tools[1], tools[0])
|
|
632
|
+
: alwaysFollowedBy(tools[0], tools[1]);
|
|
633
|
+
}
|
|
189
634
|
case "rate_limit": {
|
|
190
|
-
const n =
|
|
635
|
+
const n = parseRateLimitCount(text);
|
|
191
636
|
if (n == null)
|
|
192
637
|
continue;
|
|
193
638
|
return rateLimit(tools[0], n);
|
|
@@ -196,16 +641,147 @@ export function parseNl(text) {
|
|
|
196
641
|
return idempotent(tools[0]);
|
|
197
642
|
case "mutual_exclusion":
|
|
198
643
|
return mutualExclusion(tools[0], tools[1]);
|
|
199
|
-
case "
|
|
200
|
-
|
|
644
|
+
case "arg_blacklist": {
|
|
645
|
+
// Three ways a sentence names the field, in the order they win.
|
|
646
|
+
// Parity with Python, and the reason this side got it wrong:
|
|
647
|
+
// taking the second backticked name produced
|
|
648
|
+
// arg_field_has('bash', 'rm -rf', 'rm -rf'), a rule watching an
|
|
649
|
+
// argument nobody sends, which parses and arms and never fires.
|
|
650
|
+
//
|
|
651
|
+
// 1. marked: "tool `bash` arg `command` must not ..."
|
|
652
|
+
// 2. a cue word naming a real field
|
|
653
|
+
// 3. a second name that is not itself one of the banned shapes
|
|
654
|
+
const forbidden = extractDelimitedTail(text);
|
|
655
|
+
if (!forbidden.length)
|
|
656
|
+
continue;
|
|
657
|
+
const tool = tools[0];
|
|
658
|
+
const marked = text.match(/\b(?:arg|argument|field|param(?:eter)?)s?\s+[`"']([^`"']+)[`"']/i);
|
|
659
|
+
const cue = text.match(/\b(command|query|input|body|path|url|text|content|param|parameter)\b/i);
|
|
660
|
+
const field = marked
|
|
661
|
+
? marked[1].trim()
|
|
662
|
+
: cue
|
|
663
|
+
? cue[1].toLowerCase()
|
|
664
|
+
: tools.length >= 2 && !forbidden.includes(tools[1])
|
|
665
|
+
? tools[1]
|
|
666
|
+
: "command";
|
|
667
|
+
return argBlacklist(tool, field, forbidden);
|
|
668
|
+
}
|
|
669
|
+
case "requires_permission": {
|
|
670
|
+
// "`deploy` requires permission `approve_deploy`" names a tool,
|
|
671
|
+
// not a permission held in the agent's static definition, and
|
|
672
|
+
// ``requires_permission`` reads its atom from that definition:
|
|
673
|
+
// armed against a permission nobody grants, it fires on every
|
|
674
|
+
// call. Python routes a tool-shaped name to must_precede.
|
|
675
|
+
const perm = tools[1];
|
|
676
|
+
return perm.includes("_") || perm.endsWith("()")
|
|
677
|
+
? mustPrecede(perm, tools[0])
|
|
678
|
+
: requiresPermission(tools[0], perm);
|
|
679
|
+
}
|
|
680
|
+
case "segregation_of_duty":
|
|
681
|
+
return segregationOfDuty(tools[0], tools[1]);
|
|
682
|
+
case "scope_limit": {
|
|
683
|
+
// The roots are the path-shaped arguments; the tool is whatever
|
|
684
|
+
// else was named. "restrict file access to `/workspace`" names no
|
|
685
|
+
// tool, so the rule applies to any call carrying a path.
|
|
686
|
+
const roots = tools.filter((t) => t.startsWith("/"));
|
|
687
|
+
const named = tools.filter((t) => !t.startsWith("/"));
|
|
688
|
+
const bare = text.match(/(?:^|\s)(\/[^\s`'",;]+)/g) ?? [];
|
|
689
|
+
const paths = roots.length
|
|
690
|
+
? roots
|
|
691
|
+
: bare.map((b) => b.trim()).filter(Boolean);
|
|
692
|
+
if (!paths.length)
|
|
693
|
+
continue;
|
|
694
|
+
return scopeLimit(named[0] ?? ".*", paths);
|
|
695
|
+
}
|
|
696
|
+
case "no_reversal": {
|
|
697
|
+
// ``noReversal(commitment, contradiction)`` takes the committing
|
|
698
|
+
// action first, and English puts it last whenever the sentence
|
|
699
|
+
// opens with the prohibition: "never call `delete` after
|
|
700
|
+
// `restore`" means restore commits. Reading the names in the
|
|
701
|
+
// order they appear built the opposite rule — and this parser
|
|
702
|
+
// rewrites the desc from the args, so the console showed a rule
|
|
703
|
+
// the user never wrote. Parity with Python's
|
|
704
|
+
// ``_forbidden_action_comes_first``.
|
|
705
|
+
const swap = /must not follow|should not follow|not allowed after|forbidden after|prohibited after|never after/.test(lower) ||
|
|
706
|
+
// "never `A` after `B`" and its two siblings put the
|
|
707
|
+
// contradiction first as well, and matched none of the
|
|
708
|
+
// spellings above because the negation word is separated from
|
|
709
|
+
// "after" by the action itself.
|
|
710
|
+
/^(?:never|cannot|can\s*not|must\s+not)\b(?=.*\bafter\b)/.test(lower) ||
|
|
711
|
+
forbiddenActionComesFirst(lower, tools[0], tools[1]);
|
|
712
|
+
return swap
|
|
713
|
+
? noReversal(tools[1], tools[0])
|
|
714
|
+
: noReversal(tools[0], tools[1]);
|
|
715
|
+
}
|
|
201
716
|
case "cooldown": {
|
|
202
|
-
const n =
|
|
717
|
+
const n = parseStepCount(text);
|
|
203
718
|
if (n == null)
|
|
204
719
|
continue;
|
|
205
720
|
return cooldown(tools[0], n);
|
|
206
721
|
}
|
|
722
|
+
case "arg_allowlist": {
|
|
723
|
+
const allowed = extractAllowlistPatterns(text);
|
|
724
|
+
if (!allowed.length)
|
|
725
|
+
continue;
|
|
726
|
+
// A second name is the field; with only one name the sentence
|
|
727
|
+
// named the tool and left the field implicit, as Python does.
|
|
728
|
+
return argAllowlist(tools[0], tools[1] ?? "command", allowed);
|
|
729
|
+
}
|
|
730
|
+
case "data_intact": {
|
|
731
|
+
const paths = tools.filter((t) => t.startsWith("/"));
|
|
732
|
+
if (!paths.length)
|
|
733
|
+
continue;
|
|
734
|
+
return dataIntact(tools[0], paths);
|
|
735
|
+
}
|
|
736
|
+
case "dry_run_before_commit":
|
|
737
|
+
return dryRunBeforeCommit(tools[0], tools[1]);
|
|
738
|
+
case "backup_before_destructive":
|
|
739
|
+
return backupBeforeDestructive(tools[0], tools[1]);
|
|
740
|
+
case "audit_after":
|
|
741
|
+
// One name means the audit step is derived, the same convention
|
|
742
|
+
// Python uses, so "`deploy` must be logged" is a usable rule
|
|
743
|
+
// rather than an arity error.
|
|
744
|
+
return auditAfter(tools[0], tools[1] ?? `audit_${tools[0]}`);
|
|
745
|
+
case "approval_freshness": {
|
|
746
|
+
const steps = parseStepCount(text) ?? parseNumber(text);
|
|
747
|
+
if (steps == null)
|
|
748
|
+
continue;
|
|
749
|
+
return tools.length >= 2
|
|
750
|
+
? approvalFreshness(tools[0], tools[1], steps)
|
|
751
|
+
: approvalFreshness(`approve_${tools[0]}`, tools[0], steps);
|
|
752
|
+
}
|
|
753
|
+
case "sanitized_before_sink":
|
|
754
|
+
return sanitizedBeforeSink(tools[0], tools[1], tools[2]);
|
|
755
|
+
case "duplicate_call_limit": {
|
|
756
|
+
const n = parseRateLimitCount(text) ?? parseNumber(text);
|
|
757
|
+
if (n == null)
|
|
758
|
+
continue;
|
|
759
|
+
return duplicateCallLimit(tools[0], tools[1], n);
|
|
760
|
+
}
|
|
761
|
+
case "bounded_retry": {
|
|
762
|
+
const n = parseRetryCount(text);
|
|
763
|
+
if (n == null)
|
|
764
|
+
continue;
|
|
765
|
+
return boundedRetry(tools[0], n);
|
|
766
|
+
}
|
|
767
|
+
case "must_confirm":
|
|
768
|
+
return mustConfirm(tools[0]);
|
|
769
|
+
case "no_data_leak": {
|
|
770
|
+
// ``no_data_leak`` is about a kind of data reaching a sink. When
|
|
771
|
+
// the first name is a tool rather than a kind, the sentence is
|
|
772
|
+
// saying "having called A, do not call B", which is a reversal.
|
|
773
|
+
// Python routes it that way and this side did not, so the same
|
|
774
|
+
// rulebook produced two different patterns and the cloud counted
|
|
775
|
+
// them as two findings.
|
|
776
|
+
const DATA_KINDS = new Set([
|
|
777
|
+
"pii", "credentials", "personal_data", "sensitive", "secrets",
|
|
778
|
+
]);
|
|
779
|
+
return DATA_KINDS.has(tools[0].toLowerCase())
|
|
780
|
+
? noDataLeak(tools[0], tools[1])
|
|
781
|
+
: noReversal(tools[0], tools[1]);
|
|
782
|
+
}
|
|
207
783
|
case "deadline": {
|
|
208
|
-
const n =
|
|
784
|
+
const n = parseStepCount(text);
|
|
209
785
|
if (n == null)
|
|
210
786
|
continue;
|
|
211
787
|
// Parity with Python NL parser: ``deadline(actions[0], actions[1], n)``,
|