@interlok/cli 0.0.0-stage → 1.3.1
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/LICENSE +5 -0
- package/README.md +158 -2
- package/dist/approvals-mcp.d.ts +83 -0
- package/dist/approvals-mcp.js +185 -0
- package/dist/canonical.d.ts +1 -0
- package/dist/canonical.js +20 -0
- package/dist/classify.d.ts +107 -0
- package/dist/classify.js +524 -0
- package/dist/gate.d.ts +19 -0
- package/dist/gate.js +338 -0
- package/dist/hook.d.ts +108 -0
- package/dist/hook.js +578 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +241 -0
- package/dist/inventory.d.ts +53 -0
- package/dist/inventory.js +210 -0
- package/dist/packs.d.ts +105 -0
- package/dist/packs.js +186 -0
- package/dist/proof-verify.d.ts +108 -0
- package/dist/proof-verify.js +513 -0
- package/dist/proxy.d.ts +69 -0
- package/dist/proxy.js +361 -0
- package/dist/scan.d.ts +7 -0
- package/dist/scan.js +41 -0
- package/dist/verify.d.ts +34 -0
- package/dist/verify.js +123 -0
- package/package.json +31 -4
package/dist/classify.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool risk classifier and coverage math.
|
|
3
|
+
*
|
|
4
|
+
* Deterministic on purpose, like the posture score: the same tool must classify the same way on
|
|
5
|
+
* every machine, in the console, in CI, and in the gate. No model, no network, no clock.
|
|
6
|
+
*
|
|
7
|
+
* This file imports nothing. The public CLI vendors it byte-for-byte (cli/src/classify.ts) and a
|
|
8
|
+
* test fails the build if the two copies drift, so a CI coverage number and a console coverage
|
|
9
|
+
* number can never disagree about the same inventory.
|
|
10
|
+
*
|
|
11
|
+
* Precedence, highest first:
|
|
12
|
+
* 1. A human declaration (recorded in the ledger) wins outright.
|
|
13
|
+
* 2. The registry's write_path, when the tool is registered.
|
|
14
|
+
* 3. The higher of annotation risk and name/schema heuristics. MCP annotations come from the
|
|
15
|
+
* server and are untrusted, so they can raise risk but never lower it.
|
|
16
|
+
* 4. A tool whose name matches neither a read nor a write verb fails closed: it is treated as a
|
|
17
|
+
* high-risk write path and counted as unclassified until someone declares it.
|
|
18
|
+
*/
|
|
19
|
+
export const CLASSIFIER_VERSION = "1.0.0";
|
|
20
|
+
const TIERS = ["low", "medium", "high", "critical"];
|
|
21
|
+
export const tierRank = (t) => TIERS.indexOf(t);
|
|
22
|
+
export const maxTier = (a, b) => (tierRank(a) >= tierRank(b) ? a : b);
|
|
23
|
+
export const atLeast = (t, floor) => tierRank(t) >= tierRank(floor);
|
|
24
|
+
/* The gate's own write test, copied from write-path.ts so this file can stay import-free.
|
|
25
|
+
tests/classify.test.ts proves the invariant that matters: whenever inferWritePath calls a name a
|
|
26
|
+
write, so does the classifier. The classifier may be more cautious than the gate, never less. */
|
|
27
|
+
const WRITE = /(create|update|delete|write|send|post|put|patch|exec|run|deploy|merge|push|remove|drop|insert|upsert|transfer|pay|approve|apply|set_|modify|rollback|restart|invite|revoke|grant|publish|purge|truncate|terminate|scale|provision)/i;
|
|
28
|
+
const READ_OVERRIDE = /^(get|list|read|search|find|describe|fetch|show|query|count|head)_?/i;
|
|
29
|
+
function gateSaysWrite(name) {
|
|
30
|
+
if (READ_OVERRIDE.test(name) && !WRITE.test(name.replace(READ_OVERRIDE, "")))
|
|
31
|
+
return false;
|
|
32
|
+
return WRITE.test(name);
|
|
33
|
+
}
|
|
34
|
+
/** Verb tokens, grouped by the consequence they carry. A token matches exactly or with s/es/ed/ing. */
|
|
35
|
+
const VERBS = {
|
|
36
|
+
read: [
|
|
37
|
+
"get",
|
|
38
|
+
"list",
|
|
39
|
+
"read",
|
|
40
|
+
"search",
|
|
41
|
+
"find",
|
|
42
|
+
"describe",
|
|
43
|
+
"fetch",
|
|
44
|
+
"show",
|
|
45
|
+
"query",
|
|
46
|
+
"count",
|
|
47
|
+
"head",
|
|
48
|
+
"view",
|
|
49
|
+
"browse",
|
|
50
|
+
"lookup",
|
|
51
|
+
"inspect",
|
|
52
|
+
"check",
|
|
53
|
+
"validate",
|
|
54
|
+
"preview",
|
|
55
|
+
"summarize",
|
|
56
|
+
"summarise",
|
|
57
|
+
"analyze",
|
|
58
|
+
"analyse",
|
|
59
|
+
"diff",
|
|
60
|
+
"compare",
|
|
61
|
+
"resolve",
|
|
62
|
+
"explain",
|
|
63
|
+
"lint",
|
|
64
|
+
"screenshot",
|
|
65
|
+
"snapshot",
|
|
66
|
+
"navigate",
|
|
67
|
+
"think",
|
|
68
|
+
"sequentialthinking",
|
|
69
|
+
"status",
|
|
70
|
+
"whoami",
|
|
71
|
+
"ping",
|
|
72
|
+
"echo",
|
|
73
|
+
"health",
|
|
74
|
+
"download",
|
|
75
|
+
"retrieve",
|
|
76
|
+
"load",
|
|
77
|
+
"watch",
|
|
78
|
+
"poll",
|
|
79
|
+
"wait",
|
|
80
|
+
"calculate",
|
|
81
|
+
"convert",
|
|
82
|
+
"translate",
|
|
83
|
+
"parse",
|
|
84
|
+
"render",
|
|
85
|
+
"format",
|
|
86
|
+
"hover",
|
|
87
|
+
"scroll",
|
|
88
|
+
"estimate",
|
|
89
|
+
"verify",
|
|
90
|
+
"generate",
|
|
91
|
+
"log",
|
|
92
|
+
"show",
|
|
93
|
+
],
|
|
94
|
+
payment: [
|
|
95
|
+
"pay",
|
|
96
|
+
"transfer",
|
|
97
|
+
"refund",
|
|
98
|
+
"charge",
|
|
99
|
+
"withdraw",
|
|
100
|
+
"disburse",
|
|
101
|
+
"payout",
|
|
102
|
+
"buy",
|
|
103
|
+
"purchase",
|
|
104
|
+
"sell",
|
|
105
|
+
"wire",
|
|
106
|
+
],
|
|
107
|
+
access_grant: ["grant", "revoke", "invite", "impersonate", "share", "authorize", "unshare"],
|
|
108
|
+
destructive: [
|
|
109
|
+
"delete",
|
|
110
|
+
"remove",
|
|
111
|
+
"drop",
|
|
112
|
+
"truncate",
|
|
113
|
+
"destroy",
|
|
114
|
+
"purge",
|
|
115
|
+
"wipe",
|
|
116
|
+
"erase",
|
|
117
|
+
"terminate",
|
|
118
|
+
"kill",
|
|
119
|
+
"uninstall",
|
|
120
|
+
"reset",
|
|
121
|
+
],
|
|
122
|
+
code_exec: ["exec", "execute", "shell", "bash", "eval", "evaluate", "spawn", "sh"],
|
|
123
|
+
deploy: [
|
|
124
|
+
"deploy",
|
|
125
|
+
"release",
|
|
126
|
+
"publish",
|
|
127
|
+
"merge",
|
|
128
|
+
"push",
|
|
129
|
+
"rollback",
|
|
130
|
+
"restart",
|
|
131
|
+
"scale",
|
|
132
|
+
"provision",
|
|
133
|
+
"promote",
|
|
134
|
+
"apply",
|
|
135
|
+
"run",
|
|
136
|
+
"trigger",
|
|
137
|
+
"dispatch",
|
|
138
|
+
"start",
|
|
139
|
+
"stop",
|
|
140
|
+
"reboot",
|
|
141
|
+
"pause",
|
|
142
|
+
"resume",
|
|
143
|
+
],
|
|
144
|
+
outbound_msg: [
|
|
145
|
+
"send",
|
|
146
|
+
"post",
|
|
147
|
+
"notify",
|
|
148
|
+
"email",
|
|
149
|
+
"reply",
|
|
150
|
+
"comment",
|
|
151
|
+
"tweet",
|
|
152
|
+
"message",
|
|
153
|
+
"mail",
|
|
154
|
+
"sms",
|
|
155
|
+
"text",
|
|
156
|
+
"call",
|
|
157
|
+
"broadcast",
|
|
158
|
+
"dm",
|
|
159
|
+
],
|
|
160
|
+
external_write: [],
|
|
161
|
+
data_write: [
|
|
162
|
+
"create",
|
|
163
|
+
"update",
|
|
164
|
+
"write",
|
|
165
|
+
"put",
|
|
166
|
+
"patch",
|
|
167
|
+
"insert",
|
|
168
|
+
"upsert",
|
|
169
|
+
"set",
|
|
170
|
+
"modify",
|
|
171
|
+
"edit",
|
|
172
|
+
"add",
|
|
173
|
+
"move",
|
|
174
|
+
"rename",
|
|
175
|
+
"upload",
|
|
176
|
+
"save",
|
|
177
|
+
"commit",
|
|
178
|
+
"approve",
|
|
179
|
+
"assign",
|
|
180
|
+
"unassign",
|
|
181
|
+
"close",
|
|
182
|
+
"reopen",
|
|
183
|
+
"label",
|
|
184
|
+
"unlabel",
|
|
185
|
+
"archive",
|
|
186
|
+
"cancel",
|
|
187
|
+
"submit",
|
|
188
|
+
"click",
|
|
189
|
+
"fill",
|
|
190
|
+
"type",
|
|
191
|
+
"press",
|
|
192
|
+
"drag",
|
|
193
|
+
"replace",
|
|
194
|
+
"star",
|
|
195
|
+
"unstar",
|
|
196
|
+
"fork",
|
|
197
|
+
"lock",
|
|
198
|
+
"unlock",
|
|
199
|
+
"pin",
|
|
200
|
+
"unpin",
|
|
201
|
+
"mark",
|
|
202
|
+
"enable",
|
|
203
|
+
"disable",
|
|
204
|
+
"schedule",
|
|
205
|
+
"book",
|
|
206
|
+
"order",
|
|
207
|
+
"import",
|
|
208
|
+
"sync",
|
|
209
|
+
"restore",
|
|
210
|
+
"copy",
|
|
211
|
+
"duplicate",
|
|
212
|
+
"configure",
|
|
213
|
+
"register",
|
|
214
|
+
"unregister",
|
|
215
|
+
"subscribe",
|
|
216
|
+
"unsubscribe",
|
|
217
|
+
"react",
|
|
218
|
+
"finalize",
|
|
219
|
+
"confirm",
|
|
220
|
+
"rebase",
|
|
221
|
+
"checkout",
|
|
222
|
+
"open",
|
|
223
|
+
"select",
|
|
224
|
+
"draft",
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
/** A verbless name made only of these reads as a lookup: `directory_tree`, `git_log`, `project_info`. */
|
|
228
|
+
const READ_NOUNS = /^(tree|info|details|docs|documentation|history|logs|stats|metrics|overview|time|weather|status|version|schema|summary|usage)$/;
|
|
229
|
+
/** Nouns that sharpen a write verb: `create_payment` is a payment, `add_member` grants access. */
|
|
230
|
+
const NOUNS = [
|
|
231
|
+
[
|
|
232
|
+
/^(payment|payments|payout|payouts|transfer|transfers|refund|refunds|charge|charges|funds|money|wire|invoice_payment)$/,
|
|
233
|
+
"payment",
|
|
234
|
+
],
|
|
235
|
+
[
|
|
236
|
+
/^(member|members|role|roles|permission|permissions|collaborator|collaborators|access|acl|apikey|api_key|key|keys|token|tokens|secret|secrets|owner|admin|policy|policies)$/,
|
|
237
|
+
"access_grant",
|
|
238
|
+
],
|
|
239
|
+
[/^(command|commands|script|scripts|code|shell|terminal|cmd|process)$/, "code_exec"],
|
|
240
|
+
[
|
|
241
|
+
/^(deployment|deployments|release|releases|production|prod|workflow|workflows|pipeline|pipelines|job|jobs|build|builds|instance|instances|cluster|clusters|service|services|function|functions)$/,
|
|
242
|
+
"deploy",
|
|
243
|
+
],
|
|
244
|
+
[
|
|
245
|
+
/^(email|emails|message|messages|sms|notification|notifications|tweet|post|comment|comments|reply|dm|chat)$/,
|
|
246
|
+
"outbound_msg",
|
|
247
|
+
],
|
|
248
|
+
];
|
|
249
|
+
const SCHEMA_CLASSES = [
|
|
250
|
+
[
|
|
251
|
+
/^(amount|amount_cents|price|cost|total|currency|iban|routing|account_number|card|payment_method)$/i,
|
|
252
|
+
"financial",
|
|
253
|
+
],
|
|
254
|
+
[
|
|
255
|
+
/^(to|cc|bcc|recipient|recipients|phone|phone_number|email|email_address|channel|channel_id|user_ids?)$/i,
|
|
256
|
+
"comms",
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
/^(ssn|social_security|dob|date_of_birth|passport|address|full_name|patient|medical_record)$/i,
|
|
260
|
+
"pii",
|
|
261
|
+
],
|
|
262
|
+
[/^(password|secret|token|api_key|apikey|credential|credentials|private_key)$/i, "secrets"],
|
|
263
|
+
[/^(sql|query|statement|table|table_name|database|schema)$/i, "prod_data"],
|
|
264
|
+
];
|
|
265
|
+
const DESCRIPTION_IRREVERSIBLE = /(irreversibl|permanently|cannot be undone|can't be undone|no way to recover)/i;
|
|
266
|
+
const EFFECT_TIER = {
|
|
267
|
+
payment: "critical",
|
|
268
|
+
access_grant: "critical",
|
|
269
|
+
destructive: "critical",
|
|
270
|
+
code_exec: "critical",
|
|
271
|
+
deploy: "high",
|
|
272
|
+
outbound_msg: "high",
|
|
273
|
+
external_write: "medium",
|
|
274
|
+
data_write: "medium",
|
|
275
|
+
};
|
|
276
|
+
const CLASS_TIER = {
|
|
277
|
+
financial: "high",
|
|
278
|
+
secrets: "high",
|
|
279
|
+
prod_data: "high",
|
|
280
|
+
pii: "medium",
|
|
281
|
+
comms: "medium",
|
|
282
|
+
};
|
|
283
|
+
/** The verb part of a namespaced or dotted tool name: `github/create_release` → `create_release`. */
|
|
284
|
+
export function bareToolName(tool) {
|
|
285
|
+
const cut = Math.max(tool.lastIndexOf("/"), tool.lastIndexOf(":"), tool.lastIndexOf("."));
|
|
286
|
+
return cut >= 0 ? tool.slice(cut + 1) : tool;
|
|
287
|
+
}
|
|
288
|
+
/** camelCase and kebab-case to snake_case, so `sendEmail` and `send-email` match `send_email`. */
|
|
289
|
+
function normalize(name) {
|
|
290
|
+
return name
|
|
291
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
292
|
+
.replace(/[-\s]+/g, "_")
|
|
293
|
+
.toLowerCase();
|
|
294
|
+
}
|
|
295
|
+
const tokens = (name) => name.split(/_+/).filter(Boolean);
|
|
296
|
+
const inflect = (tok, verb) => tok === verb ||
|
|
297
|
+
tok === `${verb}s` ||
|
|
298
|
+
tok === `${verb}es` ||
|
|
299
|
+
tok === `${verb}ed` ||
|
|
300
|
+
tok === `${verb}d` ||
|
|
301
|
+
tok === `${verb}ing` ||
|
|
302
|
+
tok === `${verb.replace(/e$/, "")}ing` ||
|
|
303
|
+
tok === `${verb}${verb.slice(-1)}ing`;
|
|
304
|
+
const verbOf = (tok, group, exact = false) => VERBS[group].some((v) => (exact ? tok === v : inflect(tok, v)));
|
|
305
|
+
function schemaKeys(schema) {
|
|
306
|
+
const props = schema?.properties;
|
|
307
|
+
return props && typeof props === "object" ? Object.keys(props) : [];
|
|
308
|
+
}
|
|
309
|
+
const uniq = (xs) => [...new Set(xs)].sort();
|
|
310
|
+
const WRITE_GROUPS = [
|
|
311
|
+
"payment",
|
|
312
|
+
"access_grant",
|
|
313
|
+
"destructive",
|
|
314
|
+
"code_exec",
|
|
315
|
+
"deploy",
|
|
316
|
+
"outbound_msg",
|
|
317
|
+
"data_write",
|
|
318
|
+
];
|
|
319
|
+
export function classifyTool(t, declared) {
|
|
320
|
+
const name = normalize(bareToolName(t.name));
|
|
321
|
+
const toks = tokens(name);
|
|
322
|
+
const basis = [];
|
|
323
|
+
const effects = [];
|
|
324
|
+
const classes = [];
|
|
325
|
+
let tier = "low";
|
|
326
|
+
let reversibility = "unknown";
|
|
327
|
+
let source = "heuristic";
|
|
328
|
+
// Which verb tokens are present, by consequence. Once a read verb has appeared, a later token
|
|
329
|
+
// only counts as a write verb in its bare form: `get_or_create_user` writes, but in
|
|
330
|
+
// `list_releases` and `list_runs` the plural is a noun being listed.
|
|
331
|
+
const firstRead = toks.findIndex((tok) => verbOf(tok, "read"));
|
|
332
|
+
const firstWrite = toks.findIndex((tok) => WRITE_GROUPS.some((g) => verbOf(tok, g)));
|
|
333
|
+
const readLeads = firstRead >= 0 && (firstWrite < 0 || firstRead < firstWrite);
|
|
334
|
+
const verbEffects = WRITE_GROUPS.filter((g) => toks.some((tok, i) => verbOf(tok, g, readLeads && i > firstRead)));
|
|
335
|
+
// A verbless name that ends in a lookup noun reads: `directory_tree`, `project_info`.
|
|
336
|
+
const hasReadVerb = firstRead >= 0 || READ_NOUNS.test(toks[toks.length - 1] ?? "");
|
|
337
|
+
const gateWrite = gateSaysWrite(name);
|
|
338
|
+
let write;
|
|
339
|
+
if (verbEffects.length) {
|
|
340
|
+
// A write verb wins over a read verb: `get_or_create_user` writes.
|
|
341
|
+
write = true;
|
|
342
|
+
effects.push(...verbEffects);
|
|
343
|
+
basis.push(`write verb in "${name}"`);
|
|
344
|
+
// Nouns only sharpen a real write verb, never invent one.
|
|
345
|
+
for (const tok of toks)
|
|
346
|
+
for (const [re, e] of NOUNS)
|
|
347
|
+
if (re.test(tok))
|
|
348
|
+
effects.push(e);
|
|
349
|
+
}
|
|
350
|
+
else if (gateWrite) {
|
|
351
|
+
// The gate's substring test sees a write (e.g. "pay" inside get_payment_status) but no write
|
|
352
|
+
// verb token does. Stay at least as cautious as the gate, without inventing a consequence.
|
|
353
|
+
write = true;
|
|
354
|
+
effects.push("data_write");
|
|
355
|
+
basis.push(`the gate's write test matches "${name}"`);
|
|
356
|
+
}
|
|
357
|
+
else if (hasReadVerb) {
|
|
358
|
+
write = false;
|
|
359
|
+
basis.push(`read verb in "${name}"`);
|
|
360
|
+
}
|
|
361
|
+
else
|
|
362
|
+
write = null;
|
|
363
|
+
for (const k of schemaKeys(t.input_schema))
|
|
364
|
+
for (const [re, c] of SCHEMA_CLASSES)
|
|
365
|
+
if (re.test(k))
|
|
366
|
+
classes.push(c);
|
|
367
|
+
if (classes.length)
|
|
368
|
+
basis.push(`argument names suggest ${uniq(classes).join(", ")}`);
|
|
369
|
+
// Annotations can raise, never lower.
|
|
370
|
+
const a = t.annotations ?? {};
|
|
371
|
+
if (a.destructiveHint === true) {
|
|
372
|
+
write = true;
|
|
373
|
+
effects.push("destructive");
|
|
374
|
+
basis.push("annotated destructive");
|
|
375
|
+
source = "annotation";
|
|
376
|
+
}
|
|
377
|
+
if (a.readOnlyHint === false && write !== true) {
|
|
378
|
+
write = true;
|
|
379
|
+
effects.push("data_write");
|
|
380
|
+
basis.push("annotated not read-only");
|
|
381
|
+
source = "annotation";
|
|
382
|
+
}
|
|
383
|
+
if (a.openWorldHint === true && write) {
|
|
384
|
+
effects.push("external_write");
|
|
385
|
+
basis.push("annotated open-world");
|
|
386
|
+
}
|
|
387
|
+
// Registry write_path beats everything but a declaration.
|
|
388
|
+
if (typeof t.write_path === "boolean") {
|
|
389
|
+
if (t.write_path !== write)
|
|
390
|
+
basis.push(`registry says ${t.write_path ? "write" : "read"}`);
|
|
391
|
+
write = t.write_path;
|
|
392
|
+
source = "registry";
|
|
393
|
+
if (write && !effects.length)
|
|
394
|
+
effects.push("data_write");
|
|
395
|
+
}
|
|
396
|
+
let unclassified = false;
|
|
397
|
+
if (write === null) {
|
|
398
|
+
// Neither a read verb nor a write verb: fail closed.
|
|
399
|
+
write = true;
|
|
400
|
+
unclassified = true;
|
|
401
|
+
source = "unknown";
|
|
402
|
+
tier = "high";
|
|
403
|
+
basis.push(`no recognized verb in "${name}"; treated as a high-risk write until declared`);
|
|
404
|
+
}
|
|
405
|
+
if (write) {
|
|
406
|
+
for (const e of effects)
|
|
407
|
+
tier = maxTier(tier, EFFECT_TIER[e]);
|
|
408
|
+
for (const c of classes)
|
|
409
|
+
tier = maxTier(tier, CLASS_TIER[c]);
|
|
410
|
+
if (classes.includes("comms") && effects.length === 1 && effects[0] === "data_write") {
|
|
411
|
+
effects.push("outbound_msg");
|
|
412
|
+
tier = maxTier(tier, "high");
|
|
413
|
+
}
|
|
414
|
+
reversibility = effects.includes("destructive")
|
|
415
|
+
? "irreversible"
|
|
416
|
+
: effects.includes("payment") || effects.includes("outbound_msg")
|
|
417
|
+
? "compensable"
|
|
418
|
+
: effects.length
|
|
419
|
+
? "reversible"
|
|
420
|
+
: "unknown";
|
|
421
|
+
if (t.description && DESCRIPTION_IRREVERSIBLE.test(t.description)) {
|
|
422
|
+
reversibility = "irreversible";
|
|
423
|
+
tier = maxTier(tier, "high");
|
|
424
|
+
basis.push("description says it cannot be undone");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
tier = "low";
|
|
429
|
+
reversibility = "reversible";
|
|
430
|
+
}
|
|
431
|
+
let profile = {
|
|
432
|
+
tool: t.name,
|
|
433
|
+
write_path: write,
|
|
434
|
+
risk_tier: tier,
|
|
435
|
+
reversibility,
|
|
436
|
+
data_classes: uniq(classes),
|
|
437
|
+
side_effects: write ? uniq(effects) : [],
|
|
438
|
+
source,
|
|
439
|
+
unclassified,
|
|
440
|
+
basis,
|
|
441
|
+
classifier_version: CLASSIFIER_VERSION,
|
|
442
|
+
};
|
|
443
|
+
if (declared && Object.keys(declared).length) {
|
|
444
|
+
profile = {
|
|
445
|
+
...profile,
|
|
446
|
+
...(declared.write_path !== undefined ? { write_path: declared.write_path } : {}),
|
|
447
|
+
...(declared.risk_tier ? { risk_tier: declared.risk_tier } : {}),
|
|
448
|
+
...(declared.reversibility ? { reversibility: declared.reversibility } : {}),
|
|
449
|
+
...(declared.data_classes ? { data_classes: uniq(declared.data_classes) } : {}),
|
|
450
|
+
...(declared.side_effects ? { side_effects: uniq(declared.side_effects) } : {}),
|
|
451
|
+
source: "declared",
|
|
452
|
+
unclassified: false,
|
|
453
|
+
basis: [...basis, "declared by a person"],
|
|
454
|
+
};
|
|
455
|
+
if (!profile.write_path)
|
|
456
|
+
profile = { ...profile, risk_tier: declared.risk_tier ?? "low", side_effects: [] };
|
|
457
|
+
}
|
|
458
|
+
return profile;
|
|
459
|
+
}
|
|
460
|
+
const RUNGS = ["guarded", "exercised", "governed", "enforced"];
|
|
461
|
+
export function computeCoverage(items, threshold = "high") {
|
|
462
|
+
const byKey = new Map();
|
|
463
|
+
for (const i of items) {
|
|
464
|
+
const prev = byKey.get(i.key);
|
|
465
|
+
// The same path listed twice keeps its highest risk and any rung that is true on either.
|
|
466
|
+
if (!prev)
|
|
467
|
+
byKey.set(i.key, i);
|
|
468
|
+
else
|
|
469
|
+
byKey.set(i.key, {
|
|
470
|
+
key: i.key,
|
|
471
|
+
profile: tierRank(i.profile.risk_tier) > tierRank(prev.profile.risk_tier)
|
|
472
|
+
? i.profile
|
|
473
|
+
: prev.profile,
|
|
474
|
+
guarded: or(prev.guarded, i.guarded),
|
|
475
|
+
exercised: or(prev.exercised, i.exercised),
|
|
476
|
+
governed: or(prev.governed, i.governed),
|
|
477
|
+
enforced: or(prev.enforced, i.enforced),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
const high = [...byKey.values()]
|
|
481
|
+
.filter((i) => i.profile.write_path && atLeast(i.profile.risk_tier, threshold))
|
|
482
|
+
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
483
|
+
const rungs = {};
|
|
484
|
+
for (const r of RUNGS) {
|
|
485
|
+
if (!high.some((i) => i[r] !== undefined) && !items.some((i) => i[r] !== undefined))
|
|
486
|
+
continue;
|
|
487
|
+
const covered = high.filter((i) => i[r] === true).length;
|
|
488
|
+
rungs[r] = {
|
|
489
|
+
covered,
|
|
490
|
+
total: high.length,
|
|
491
|
+
pct: high.length ? Math.round((covered / high.length) * 100) : null,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
classifier_version: CLASSIFIER_VERSION,
|
|
496
|
+
threshold,
|
|
497
|
+
high_risk_write_paths: high.length,
|
|
498
|
+
unclassified: high.filter((i) => i.profile.unclassified).length,
|
|
499
|
+
rungs,
|
|
500
|
+
paths: high.map((i) => ({
|
|
501
|
+
key: i.key,
|
|
502
|
+
risk_tier: i.profile.risk_tier,
|
|
503
|
+
unclassified: i.profile.unclassified,
|
|
504
|
+
...(i.guarded !== undefined ? { guarded: i.guarded } : {}),
|
|
505
|
+
...(i.exercised !== undefined ? { exercised: i.exercised } : {}),
|
|
506
|
+
...(i.governed !== undefined ? { governed: i.governed } : {}),
|
|
507
|
+
...(i.enforced !== undefined ? { enforced: i.enforced } : {}),
|
|
508
|
+
})),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function or(a, b) {
|
|
512
|
+
if (a === undefined && b === undefined)
|
|
513
|
+
return undefined;
|
|
514
|
+
return a === true || b === true;
|
|
515
|
+
}
|
|
516
|
+
/** One line a person can read: "guarded 3/8 (38%) · 2 unclassified". */
|
|
517
|
+
export function coverageLine(c, rung = "guarded") {
|
|
518
|
+
const r = c.rungs[rung];
|
|
519
|
+
if (!c.high_risk_write_paths)
|
|
520
|
+
return "no high-risk write paths found";
|
|
521
|
+
const pct = r?.pct === null || r?.pct === undefined ? "—" : `${r.pct}%`;
|
|
522
|
+
const un = c.unclassified ? ` · ${c.unclassified} unclassified` : "";
|
|
523
|
+
return `${rung} ${r?.covered ?? 0}/${c.high_risk_write_paths} (${pct})${un}`;
|
|
524
|
+
}
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function collectContext(agentIds?: string[]): {
|
|
2
|
+
repo: string | null;
|
|
3
|
+
ref: string | null;
|
|
4
|
+
commit_sha: string | null;
|
|
5
|
+
pr_number: number | null;
|
|
6
|
+
requester: string | null;
|
|
7
|
+
author_kinds: string[];
|
|
8
|
+
author_ids: string[];
|
|
9
|
+
changed_paths: string[];
|
|
10
|
+
files_changed: number;
|
|
11
|
+
source_ref: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function planFromFile(path: string): {
|
|
14
|
+
summary: import("./packs.js").PlanSummary;
|
|
15
|
+
plan_hash: string;
|
|
16
|
+
};
|
|
17
|
+
/** `--action model.promote` flags → the model description the Change Gate judges. */
|
|
18
|
+
export declare function modelFromFlags(value: (n: string) => string | undefined): string | Record<string, unknown>;
|
|
19
|
+
export declare function gateCommand(args: string[], value: (n: string) => string | undefined, has: (n: string) => boolean): Promise<void>;
|