@atbash/cli 0.5.15 → 0.6.0-dev.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.
Files changed (52) hide show
  1. package/README.md +338 -0
  2. package/dist/bin/atbash.js +34 -2
  3. package/dist/bin/atbash.js.map +1 -1
  4. package/dist/commands/connect.d.ts +55 -1
  5. package/dist/commands/connect.js +11 -8
  6. package/dist/commands/connect.js.map +1 -1
  7. package/dist/commands/github-scan.d.ts +104 -0
  8. package/dist/commands/github-scan.js +1054 -0
  9. package/dist/commands/github-scan.js.map +1 -0
  10. package/dist/commands/held.js +7 -2
  11. package/dist/commands/held.js.map +1 -1
  12. package/dist/commands/history.js +4 -1
  13. package/dist/commands/history.js.map +1 -1
  14. package/dist/commands/judge-options.d.ts +21 -0
  15. package/dist/commands/judge-options.js +24 -0
  16. package/dist/commands/judge-options.js.map +1 -0
  17. package/dist/commands/judge.js +4 -1
  18. package/dist/commands/judge.js.map +1 -1
  19. package/dist/commands/policy.js +4 -1
  20. package/dist/commands/policy.js.map +1 -1
  21. package/dist/commands/scan.d.ts +45 -0
  22. package/dist/commands/scan.js +301 -0
  23. package/dist/commands/scan.js.map +1 -0
  24. package/dist/commands/setup.d.ts +151 -0
  25. package/dist/commands/setup.js +944 -0
  26. package/dist/commands/setup.js.map +1 -0
  27. package/dist/commands/stats.js +3 -1
  28. package/dist/commands/stats.js.map +1 -1
  29. package/dist/commands/status.js +4 -1
  30. package/dist/commands/status.js.map +1 -1
  31. package/dist/commands/tier.js +4 -1
  32. package/dist/commands/tier.js.map +1 -1
  33. package/dist/commands/tools.js +11 -4
  34. package/dist/commands/tools.js.map +1 -1
  35. package/dist/commands/whoami.js +4 -1
  36. package/dist/commands/whoami.js.map +1 -1
  37. package/dist/lib/grade-llm.d.ts +38 -0
  38. package/dist/lib/grade-llm.js +64 -0
  39. package/dist/lib/grade-llm.js.map +1 -0
  40. package/dist/lib/llm.d.ts +34 -0
  41. package/dist/lib/llm.js +101 -0
  42. package/dist/lib/llm.js.map +1 -0
  43. package/dist/lib/policy.d.ts +40 -0
  44. package/dist/lib/policy.js +94 -0
  45. package/dist/lib/policy.js.map +1 -0
  46. package/dist/lib/risk.d.ts +112 -0
  47. package/dist/lib/risk.js +288 -0
  48. package/dist/lib/risk.js.map +1 -0
  49. package/dist/lib/threats-llm.d.ts +39 -0
  50. package/dist/lib/threats-llm.js +72 -0
  51. package/dist/lib/threats-llm.js.map +1 -0
  52. package/package.json +3 -3
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.suggestPolicy = suggestPolicy;
4
+ /**
5
+ * Scan → starter policy.
6
+ *
7
+ * Turns the risk report into a first-draft governance boundary: a mandate line,
8
+ * Red Lines (block/jail paths), Approval boundaries (hold paths), and always the
9
+ * self-modification containment rule. This is what makes a scan ACTIONABLE rather
10
+ * than a read-only inventory. Deterministic, grouped by category, and needs no
11
+ * API key.
12
+ *
13
+ * PROVENANCE: ported from the dashboard's `src/lib/scan/policy-suggest.ts`.
14
+ * Suggesting a boundary is not the same as deploying one — deploying stays a
15
+ * dashboard function, and this command never writes a policy anywhere.
16
+ *
17
+ * Divergences from the dashboard version, all found against real output:
18
+ * - Takes the agent's declared instructions directly instead of a
19
+ * `CapabilityManifest`, because the CLI has no manifest type.
20
+ * - A TOOL that already carries a red line elsewhere in the same category is
21
+ * dropped from that category's approvals, not the whole category. Upstream
22
+ * emits both unconditionally, producing "never perform data access / egress"
23
+ * and "require human approval before data access / egress" for the SAME tool
24
+ * in one boundary. An earlier version of this fix dropped the whole category
25
+ * instead of just that tool — which silently discarded a DIFFERENT tool's
26
+ * real, distinct hold finding whenever it shared a category name with
27
+ * anything blocked (found against a real `export_crm_contacts` +
28
+ * `delete_customer_records` surface: the export tool's approval line vanished
29
+ * entirely, with no trace of it anywhere in the printed boundary). The
30
+ * collision check itself must key on source+name, not name alone — otherwise
31
+ * a blocked `x` on one MCP server can wrongly erase a held `x` on another.
32
+ * - Drops the appended word "actions", which doubled up on every category that
33
+ * already ended in one ("destructive action actions").
34
+ * - Prefers primary paths when picking example tool names.
35
+ */
36
+ const risk_1 = require("./risk");
37
+ /**
38
+ * Cap and de-duplicate example tool names per category.
39
+ *
40
+ * PRIMARY paths first: a tool that belongs to a category only as a SECONDARY harm
41
+ * is a real member of it, but a confusing example. `delete-customer-records`
42
+ * carries a data-egress harm, yet naming it as the example for data egress when
43
+ * `export-crm-contacts` is right there reads like a misclassification.
44
+ */
45
+ function examplesFor(paths) {
46
+ const ordered = [...paths].sort((a, b) => Number(!!a.secondary) - Number(!!b.secondary));
47
+ const names = [...new Set(ordered.map((p) => p.tool))];
48
+ return names.slice(0, 3).join(", ") + (names.length > 3 ? ", …" : "");
49
+ }
50
+ function suggestPolicy(risk, systemPrompt) {
51
+ // Prefer the agent's own declared instructions. Otherwise a neutral, editable
52
+ // default — NEVER built from risk category names, which are what the agent must
53
+ // avoid, not its operating scope.
54
+ const mandate = systemPrompt?.trim() ||
55
+ "Use only the connected tools, and only as needed to fulfil the assigned task. (Edit to state the specific outcome this agent owns.)";
56
+ const byCategory = (verdicts) => {
57
+ const groups = new Map();
58
+ for (const p of risk.paths) {
59
+ if (!verdicts.includes(p.recommendedVerdict))
60
+ continue;
61
+ const arr = groups.get(p.category) ?? [];
62
+ arr.push({ tool: p.tool, source: p.source, secondary: p.secondary });
63
+ groups.set(p.category, arr);
64
+ }
65
+ return groups;
66
+ };
67
+ const redLineGroups = byCategory(["block", "jail"]);
68
+ const redLines = Array.from(redLineGroups.entries()).map(([cat, tools]) => `Never perform ${cat} autonomously (e.g. ${examplesFor(tools)}).`);
69
+ // Governance staple — always present.
70
+ redLines.push("Never modify its own mandate, red lines, or authority.");
71
+ const approvals = Array.from(byCategory(["hold"]).entries())
72
+ // Most-restrictive-wins, PER TOOL. A tool's verdict is uniform across every
73
+ // path it appears in (classify() forces every secondary to the primary's
74
+ // verdict), so the only way the SAME tool shows up here AND in redLineGroups
75
+ // for this category is if it can't — it can't. What DOES happen: two
76
+ // DIFFERENT tools land in the same category, one blocked, one merely held.
77
+ // Dropping the whole category then would erase the held tool's real finding
78
+ // with no trace anywhere in the boundary — worse than the "never X"/"ask
79
+ // before X" wording clash this exists to avoid. So only drop the specific
80
+ // tool names the category's red line already names; a different tool's hold
81
+ // finding survives under its own example.
82
+ .map(([cat, tools]) => {
83
+ // Keyed on source+name: a block/jail path in this category belongs to a
84
+ // SPECIFIC tool instance, and only that instance should drop out of the
85
+ // approvals list here — not any other tool that merely shares its name.
86
+ const alreadyRedLined = new Set((redLineGroups.get(cat) ?? []).map((t) => (0, risk_1.toolKey)(t.source, t.tool)));
87
+ const remaining = tools.filter((t) => !alreadyRedLined.has((0, risk_1.toolKey)(t.source, t.tool)));
88
+ return remaining.length > 0 ? `Require human approval before ${cat} (e.g. ${examplesFor(remaining)}).` : null;
89
+ })
90
+ .filter((line) => line !== null);
91
+ const notes = `Draft boundary generated from the ${risk.method === "llm" ? "model-analyzed" : "scanned"} capability surface (${risk.paths.length} paths). Review before enforcing — it reflects authorized exposure, not observed behavior.`;
92
+ return { mandate, redLines, approvals, notes };
93
+ }
94
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/lib/policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Deterministic risk / blast-radius derivation for a locally scanned surface.
3
+ *
4
+ * Turns a tool surface into the "what could go wrong" report: each tool is
5
+ * matched to a consequence class, a severity, and a recommended runtime verdict.
6
+ * It reasons only over the DECLARED surface — what the agent is authorized to
7
+ * attempt — which is what a scan authoritatively knows. It never claims
8
+ * certainty, and that framing is what keeps a governance report honest.
9
+ *
10
+ * PROVENANCE: ported from the Atbash dashboard's `src/lib/scan/risk.ts`. The
11
+ * rules table, its ORDER, the severity weights and the scoring band are kept
12
+ * faithful on purpose: a local `atbash scan` that graded a surface differently
13
+ * from the hosted scan would be worse than no local scan at all.
14
+ *
15
+ * DIVERGENCE FROM THE DASHBOARD (deliberate, since the CLI now owns this copy):
16
+ * - `call|calls` removed from external communication, replaced with `phone\w*`.
17
+ * In a tool catalog "calls" overwhelmingly means FUNCTION calls: a real
18
+ * `js_reset` whose description read "bindings created by prior `js` calls"
19
+ * was graded as messaging outsiders. "phone" carries the intent unambiguously.
20
+ * - `add|adds|added|adding` added to state change. A real
21
+ * `js_add_node_module_dir` — which mutates module resolution, and so controls
22
+ * what code can be imported — was graded low/read-only on the word "search"
23
+ * in "module search roots". Spelled out rather than `add\w*`, which would
24
+ * match "address" and "additional".
25
+ * Both were found against a real Codex `node_repl` surface. If these are ported
26
+ * back upstream, delete this block rather than letting the two engines drift.
27
+ *
28
+ * Two further mechanical divergences:
29
+ * - `deriveRisk` takes a tool list rather than a `CapabilityManifest`; the CLI
30
+ * has no manifest type and only `.tools` was ever read.
31
+ * - `ScanTool` is declared here instead of imported, since only two of its
32
+ * fields are used.
33
+ *
34
+ * If the scan logic is ever extracted into a shared package, this file is what
35
+ * gets deleted — nothing else in the CLI depends on its internals.
36
+ */
37
+ /** One tool/capability the agent can invoke. */
38
+ export interface ScanTool {
39
+ name: string;
40
+ description?: string;
41
+ /** Which config/server surfaced this tool (provenance). */
42
+ source: string;
43
+ }
44
+ /**
45
+ * Identity key for a specific tool INSTANCE — source + name, not name alone.
46
+ *
47
+ * Two different MCP servers can each expose a tool called e.g. "search": they are
48
+ * different capabilities that happen to share a name. Anything that maps a tool
49
+ * to a per-tool result (which of its paths is unclassified, what an LLM pass
50
+ * graded it) must key on this, or one server's tool silently absorbs another's
51
+ * classification/grade.
52
+ */
53
+ export declare function toolKey(source: string | undefined, name: string): string;
54
+ export type Verdict = "allow" | "hold" | "block" | "jail";
55
+ export type Severity = "low" | "medium" | "high";
56
+ export interface RiskPath {
57
+ tool: string;
58
+ /** An ADDITIONAL harm this tool matched beyond its primary one. Governed like
59
+ * any other path, but deliberately NOT scored — see summarizePaths. */
60
+ secondary?: boolean;
61
+ category: string;
62
+ severity: Severity;
63
+ recommendedVerdict: Verdict;
64
+ consequence: string;
65
+ /** Where the tool came from, so a report can name the offending config. */
66
+ source?: string;
67
+ }
68
+ export interface RiskReport {
69
+ paths: RiskPath[];
70
+ score: number;
71
+ level: "Low" | "Elevated" | "High";
72
+ summary: string;
73
+ /** How the report was produced. The LLM pass sets "llm"; default deterministic. */
74
+ method?: "deterministic" | "llm";
75
+ }
76
+ export declare const UNCLASSIFIED_CATEGORY = "general action";
77
+ export declare const UNCLASSIFIED_CONSEQUENCE = "no specific high-risk class matched \u2014 allowed and monitored";
78
+ /**
79
+ * Marks the fall-through rule. A tool that matched NOTHING is unclassified — it
80
+ * is not a considered "medium risk" finding, and presenting it as one turns every
81
+ * unrecognized plugin into a false signal.
82
+ */
83
+ export declare function isUnclassifiedPath(path: {
84
+ category?: string;
85
+ consequence?: string;
86
+ }): boolean;
87
+ /** Score + level + summary from a set of paths — shared by both passes. */
88
+ export declare function summarizePaths(paths: RiskPath[]): Pick<RiskReport, "score" | "level" | "summary">;
89
+ /** Derive the blast-radius report from a scanned tool surface (deterministic). */
90
+ export declare function deriveRisk(tools: ScanTool[]): RiskReport;
91
+ /**
92
+ * Fill in the paths the keyword rules could not classify, using a model's per-tool
93
+ * grade and its plain-English effect.
94
+ *
95
+ * Gap-filling ONLY: a path that matched a real rule is returned untouched, so the
96
+ * deterministic engine stays the auditable floor and an LLM can never downgrade a
97
+ * known-dangerous capability. `critical` folds to `high` because that is the top
98
+ * of this module's scale. The score is recomputed so the level reflects the
99
+ * enriched set.
100
+ *
101
+ * `grades` is keyed by `${source}::${name}` (see `toolKey`), not name alone: two
102
+ * different MCP servers can expose a same-named tool, and a name-only key would
103
+ * apply one tool's grade to the other's unrelated path.
104
+ *
105
+ * Returns the ORIGINAL report object when nothing was filled, so a caller can test
106
+ * `graded !== report` to know whether the pass actually contributed.
107
+ */
108
+ export declare function gradeUnclassifiedPaths(report: RiskReport, grades: Record<string, {
109
+ severity?: string;
110
+ effect?: string;
111
+ policyGroup?: string;
112
+ }>): RiskReport;
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ /**
3
+ * Deterministic risk / blast-radius derivation for a locally scanned surface.
4
+ *
5
+ * Turns a tool surface into the "what could go wrong" report: each tool is
6
+ * matched to a consequence class, a severity, and a recommended runtime verdict.
7
+ * It reasons only over the DECLARED surface — what the agent is authorized to
8
+ * attempt — which is what a scan authoritatively knows. It never claims
9
+ * certainty, and that framing is what keeps a governance report honest.
10
+ *
11
+ * PROVENANCE: ported from the Atbash dashboard's `src/lib/scan/risk.ts`. The
12
+ * rules table, its ORDER, the severity weights and the scoring band are kept
13
+ * faithful on purpose: a local `atbash scan` that graded a surface differently
14
+ * from the hosted scan would be worse than no local scan at all.
15
+ *
16
+ * DIVERGENCE FROM THE DASHBOARD (deliberate, since the CLI now owns this copy):
17
+ * - `call|calls` removed from external communication, replaced with `phone\w*`.
18
+ * In a tool catalog "calls" overwhelmingly means FUNCTION calls: a real
19
+ * `js_reset` whose description read "bindings created by prior `js` calls"
20
+ * was graded as messaging outsiders. "phone" carries the intent unambiguously.
21
+ * - `add|adds|added|adding` added to state change. A real
22
+ * `js_add_node_module_dir` — which mutates module resolution, and so controls
23
+ * what code can be imported — was graded low/read-only on the word "search"
24
+ * in "module search roots". Spelled out rather than `add\w*`, which would
25
+ * match "address" and "additional".
26
+ * Both were found against a real Codex `node_repl` surface. If these are ported
27
+ * back upstream, delete this block rather than letting the two engines drift.
28
+ *
29
+ * Two further mechanical divergences:
30
+ * - `deriveRisk` takes a tool list rather than a `CapabilityManifest`; the CLI
31
+ * has no manifest type and only `.tools` was ever read.
32
+ * - `ScanTool` is declared here instead of imported, since only two of its
33
+ * fields are used.
34
+ *
35
+ * If the scan logic is ever extracted into a shared package, this file is what
36
+ * gets deleted — nothing else in the CLI depends on its internals.
37
+ */
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.UNCLASSIFIED_CONSEQUENCE = exports.UNCLASSIFIED_CATEGORY = void 0;
40
+ exports.toolKey = toolKey;
41
+ exports.isUnclassifiedPath = isUnclassifiedPath;
42
+ exports.summarizePaths = summarizePaths;
43
+ exports.deriveRisk = deriveRisk;
44
+ exports.gradeUnclassifiedPaths = gradeUnclassifiedPaths;
45
+ /**
46
+ * Identity key for a specific tool INSTANCE — source + name, not name alone.
47
+ *
48
+ * Two different MCP servers can each expose a tool called e.g. "search": they are
49
+ * different capabilities that happen to share a name. Anything that maps a tool
50
+ * to a per-tool result (which of its paths is unclassified, what an LLM pass
51
+ * graded it) must key on this, or one server's tool silently absorbs another's
52
+ * classification/grade.
53
+ */
54
+ function toolKey(source, name) {
55
+ return `${source ?? ""}::${name}`;
56
+ }
57
+ // Ordered most-severe first; first match wins. Patterns run against
58
+ // "name description" lowercased and tokenized (see classify()).
59
+ //
60
+ // Every keyword is \b-anchored on BOTH sides, either as a whole token or as an
61
+ // explicit stem (`deploy\w*`). Bare substrings look equivalent but are not: they
62
+ // fire on unrelated words that merely contain the keyword, and because the rules
63
+ // are ordered most-severe-first, a stray substring hit OUTRANKS the correct rule.
64
+ // Real misclassifications this prevents: "design_review"/"assign_reviewer" ->
65
+ // binding commitment (`sign`), "get_executive_summary" -> runtime execution
66
+ // (`exec`), "introduce_yourself" -> containment/jail (`self`), "list_assets" and
67
+ // "get_settings" -> state change (`set`), "postgres_list_tables" -> external
68
+ // communication (`post`), "recall_conversation" -> external communication (`call`).
69
+ const RULES = [
70
+ {
71
+ test: /\b(polic(y|ies)|mandate|red\s?line|guardrail|self|jailbreak|disabl\w*)\b|\b(escalat\w*\s+privilege|privilege\s+escalat\w*)/,
72
+ category: "self-modification / containment",
73
+ severity: "high",
74
+ verdict: "jail",
75
+ consequence: "could weaken or bypass its own authority — loss of control",
76
+ },
77
+ {
78
+ test: /\b(delete\w*|drop|destroy\w*|rm|remove\w*|force\s?push|wipe|truncate|revoke\w*)\b/,
79
+ category: "destructive action",
80
+ severity: "high",
81
+ verdict: "block",
82
+ consequence: "irreversible data/infrastructure loss",
83
+ },
84
+ {
85
+ test: /\b(pay|payment\w*|transfer\w*|refund\w*|reward\w*|charge\w*|invoice\w*|payout\w*|wire|disburse\w*)\b/,
86
+ category: "financial",
87
+ severity: "high",
88
+ verdict: "hold",
89
+ consequence: "direct financial loss",
90
+ },
91
+ {
92
+ test: /\b(shell|exec|execut(e|es|ed|ing|ion)|command|bash|python|code\s?interpreter|git|deploy\w*|terraform)\b/,
93
+ category: "runtime execution",
94
+ severity: "high",
95
+ verdict: "block",
96
+ consequence: "arbitrary code / infra actions at the runtime boundary",
97
+ },
98
+ // The read-proximity clause is bounded to one intervening word: "read customer
99
+ // records" is data egress, but "read a document ... for the user" is not — an
100
+ // unbounded .* made every read tool whose description says "the user" high-severity.
101
+ {
102
+ test: /\b(export\w*|download\w*|scrape\w*|dump|crm|database|sql|quer(y|ies))\b|\bread\w*\s+(\w+\s+)?(customer|user|pii|record)/,
103
+ category: "data access / egress",
104
+ severity: "high",
105
+ verdict: "hold",
106
+ consequence: "data leakage / privacy breach",
107
+ },
108
+ {
109
+ test: /\b(sign|signs|signed|signature|contract\w*|agree\w*|commit|commits|committed|commitment|order|orders|purchase\w*|book)\b|\bapprove\s?terms/,
110
+ category: "binding commitment",
111
+ severity: "high",
112
+ verdict: "hold",
113
+ consequence: "unauthorized commitment / liability",
114
+ },
115
+ // Autonomy-first: BLOCK (auto-deny) and ALLOW (auto-proceed, monitored) are both
116
+ // AUTOMATIC — the agent gets a decision without a human. HOLD is the narrow
117
+ // "pause for a human" set, reserved above for the truly irreversible
118
+ // external-harm categories. Reversible, monitorable actions default to ALLOW so
119
+ // a governed agent stays autonomous instead of routing half its work to a human.
120
+ {
121
+ test: /\b(send\w*|email\w*|message\w*|post|posts|publish\w*|tweet\w*|notif(y|ies|ication\w*)|slack|sms|phone\w*|contact\w*|broadcast\w*)\b/,
122
+ category: "external communication",
123
+ severity: "medium",
124
+ verdict: "allow",
125
+ consequence: "external message sent on the owner's behalf (monitored)",
126
+ },
127
+ {
128
+ test: /\b(updat\w*|modif(y|ies|ied)|edit\w*|writ\w*|creat\w*|set|chang\w*|schedul\w*|move|moves|rename\w*|add|adds|added|adding)\b/,
129
+ category: "state change",
130
+ severity: "medium",
131
+ verdict: "allow",
132
+ consequence: "changes records or state (monitored; reversible in most systems)",
133
+ },
134
+ {
135
+ test: /\b(search\w*|brows\w*|fetch\w*|lookup|look\s?up|retriev\w*|recall\w*|get|list|lists|read|describe\w*|show\w*|view\w*|summar\w*)\b/,
136
+ category: "read / research",
137
+ severity: "low",
138
+ verdict: "allow",
139
+ consequence: "low-consequence information access",
140
+ },
141
+ ];
142
+ /** The read-only class. Meaningful as a primary, never as an added finding. */
143
+ const READ_ONLY_CATEGORY = "read / research";
144
+ exports.UNCLASSIFIED_CATEGORY = "general action";
145
+ exports.UNCLASSIFIED_CONSEQUENCE = "no specific high-risk class matched — allowed and monitored";
146
+ /**
147
+ * Marks the fall-through rule. A tool that matched NOTHING is unclassified — it
148
+ * is not a considered "medium risk" finding, and presenting it as one turns every
149
+ * unrecognized plugin into a false signal.
150
+ */
151
+ function isUnclassifiedPath(path) {
152
+ return path.category === exports.UNCLASSIFIED_CATEGORY && path.consequence === exports.UNCLASSIFIED_CONSEQUENCE;
153
+ }
154
+ const DEFAULT_RULE = {
155
+ category: exports.UNCLASSIFIED_CATEGORY,
156
+ // Autonomy-first: an unclassified action defaults to ALLOW (auto, monitored)
157
+ // rather than routing every unrecognized tool to a human.
158
+ severity: "medium",
159
+ verdict: "allow",
160
+ consequence: exports.UNCLASSIFIED_CONSEQUENCE,
161
+ };
162
+ const SEV_WEIGHT = { low: 3, medium: 8, high: 16 };
163
+ // Score band per worst-severity-present. The worst severity on the surface picks
164
+ // the BAND; breadth only moves the score within it. Summing weights across all
165
+ // paths instead lets count outrank severity: 20 read-only tools (20x3=60) used to
166
+ // outscore two tools that permanently delete data (2x16=32), so a read-only agent
167
+ // read "High" while a destructive one read "Elevated".
168
+ const SEV_BAND = { low: [0, 32], medium: [33, 65], high: [66, 100] };
169
+ function pathFromRule(tool, rule, secondary) {
170
+ return {
171
+ tool: tool.name,
172
+ category: rule.category,
173
+ severity: rule.severity,
174
+ recommendedVerdict: rule.verdict,
175
+ consequence: rule.consequence,
176
+ source: tool.source,
177
+ ...(secondary ? { secondary: true } : {}),
178
+ };
179
+ }
180
+ /**
181
+ * Every consequence class this tool belongs to, most important first.
182
+ *
183
+ * The first match is the PRIMARY path and is what the score is built on. The rest
184
+ * are harms that would otherwise be dropped — a tool is rarely one kind of danger
185
+ * (`delete_repo_and_notify` destroys data AND messages outsiders). Every secondary
186
+ * is emitted at the PRIMARY's verdict: the rules are ordered by importance, not by
187
+ * verdict, so letting a secondary keep its own could hand a tool a BLOCK its
188
+ * primary never gave it — and letting it come in weaker would list the same tool
189
+ * as both a red line and an allowed limit.
190
+ */
191
+ function classify(tool) {
192
+ // Normalize non-alphanumerics to spaces so snake_case/camel/kebab tool names
193
+ // tokenize ("issue_refund" -> "issue refund"). This lets the \b anchors match
194
+ // keywords inside identifiers while keeping short tokens like "rm"/"git" safe
195
+ // (they only match as standalone words, not inside "form").
196
+ const hay = ` ${`${tool.name} ${tool.description ?? ""}`.toLowerCase().replace(/[^a-z0-9]+/g, " ")} `;
197
+ const matched = RULES.filter((r) => r.test.test(hay));
198
+ const primaryRule = matched[0] ?? DEFAULT_RULE;
199
+ const primary = pathFromRule(tool, primaryRule);
200
+ const seen = new Set([primary.category]);
201
+ const secondary = [];
202
+ for (const rule of matched.slice(1)) {
203
+ // One statement per consequence class.
204
+ if (seen.has(rule.category))
205
+ continue;
206
+ // "Also reads" is not a finding: every tool that writes, sends or deletes also
207
+ // matches the read rule, so as a SECONDARY it covers most of the surface and
208
+ // tells the owner nothing.
209
+ if (rule.category === READ_ONLY_CATEGORY)
210
+ continue;
211
+ seen.add(rule.category);
212
+ secondary.push(pathFromRule(tool, { ...rule, verdict: primaryRule.verdict }, true));
213
+ }
214
+ return [primary, ...secondary];
215
+ }
216
+ /** Score + level + summary from a set of paths — shared by both passes. */
217
+ function summarizePaths(paths) {
218
+ // Secondary paths describe a tool more precisely; they do not add capability.
219
+ // Counting them would raise the score of an unchanged runtime purely because
220
+ // the classifier learned to name a second harm.
221
+ const scored = paths.filter((p) => !p.secondary);
222
+ const sorted = [...scored].sort((a, b) => SEV_WEIGHT[b.severity] - SEV_WEIGHT[a.severity]);
223
+ const worst = sorted[0]?.severity;
224
+ const raw = sorted.reduce((sum, p) => sum + SEV_WEIGHT[p.severity], 0);
225
+ const [floor, ceil] = worst ? SEV_BAND[worst] : [0, 0];
226
+ const score = Math.min(100, Math.round(floor + (ceil - floor) * (1 - Math.exp(-raw / 40))));
227
+ const level = score >= 66 ? "High" : score >= 33 ? "Elevated" : "Low";
228
+ const highs = sorted.filter((p) => p.severity === "high").length;
229
+ const summary = sorted.length === 0
230
+ ? "No tool surface was resolved, so no blast radius could be derived. Behavior must be observed at the runtime boundary."
231
+ : `${sorted.length} authorized action path${sorted.length === 1 ? "" : "s"} (${highs} high-severity). This is what the agent is authorized to attempt before any boundary — not a prediction of intent.`;
232
+ return { score, level, summary };
233
+ }
234
+ /** Derive the blast-radius report from a scanned tool surface (deterministic). */
235
+ function deriveRisk(tools) {
236
+ const paths = tools
237
+ .flatMap(classify)
238
+ // Stable: equal severities keep the order the tools were scanned in, so a
239
+ // rescan of an unchanged runtime produces an identical report.
240
+ .sort((a, b) => SEV_WEIGHT[b.severity] - SEV_WEIGHT[a.severity]);
241
+ return { paths, ...summarizePaths(paths), method: "deterministic" };
242
+ }
243
+ /**
244
+ * Fill in the paths the keyword rules could not classify, using a model's per-tool
245
+ * grade and its plain-English effect.
246
+ *
247
+ * Gap-filling ONLY: a path that matched a real rule is returned untouched, so the
248
+ * deterministic engine stays the auditable floor and an LLM can never downgrade a
249
+ * known-dangerous capability. `critical` folds to `high` because that is the top
250
+ * of this module's scale. The score is recomputed so the level reflects the
251
+ * enriched set.
252
+ *
253
+ * `grades` is keyed by `${source}::${name}` (see `toolKey`), not name alone: two
254
+ * different MCP servers can expose a same-named tool, and a name-only key would
255
+ * apply one tool's grade to the other's unrelated path.
256
+ *
257
+ * Returns the ORIGINAL report object when nothing was filled, so a caller can test
258
+ * `graded !== report` to know whether the pass actually contributed.
259
+ */
260
+ function gradeUnclassifiedPaths(report, grades) {
261
+ let filled = 0;
262
+ const paths = report.paths.map((path) => {
263
+ if (!isUnclassifiedPath(path))
264
+ return path;
265
+ const hint = grades[toolKey(path.source, path.tool)];
266
+ const graded = (hint?.severity ?? "").toLowerCase();
267
+ const severity = graded === "critical" || graded === "high"
268
+ ? "high"
269
+ : graded === "medium"
270
+ ? "medium"
271
+ : graded === "low"
272
+ ? "low"
273
+ : undefined;
274
+ if (!severity)
275
+ return path;
276
+ filled++;
277
+ return {
278
+ ...path,
279
+ severity,
280
+ category: hint?.policyGroup?.trim() || path.category,
281
+ consequence: hint?.effect?.trim() || path.consequence,
282
+ };
283
+ });
284
+ if (!filled)
285
+ return report;
286
+ return { ...report, paths, ...summarizePaths(paths), method: "llm" };
287
+ }
288
+ //# sourceMappingURL=risk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"risk.js","sourceRoot":"","sources":["../../src/lib/risk.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;;;AAmBH,0BAEC;AAoID,gDAEC;AAqED,wCAiBC;AAGD,gCAOC;AAmBD,wDA4BC;AAhSD;;;;;;;;GAQG;AACH,SAAgB,OAAO,CAAC,MAA0B,EAAE,IAAY;IAC9D,OAAO,GAAG,MAAM,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;AACpC,CAAC;AAmCD,oEAAoE;AACpE,gEAAgE;AAChE,EAAE;AACF,+EAA+E;AAC/E,iFAAiF;AACjF,iFAAiF;AACjF,kFAAkF;AAClF,8EAA8E;AAC9E,4EAA4E;AAC5E,iFAAiF;AACjF,6EAA6E;AAC7E,oFAAoF;AACpF,MAAM,KAAK,GAAW;IACpB;QACE,IAAI,EAAE,4HAA4H;QAClI,QAAQ,EAAE,iCAAiC;QAC3C,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,MAAM;QACf,WAAW,EAAE,4DAA4D;KAC1E;IACD;QACE,IAAI,EAAE,mFAAmF;QACzF,QAAQ,EAAE,oBAAoB;QAC9B,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,uCAAuC;KACrD;IACD;QACE,IAAI,EAAE,sGAAsG;QAC5G,QAAQ,EAAE,WAAW;QACrB,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,MAAM;QACf,WAAW,EAAE,uBAAuB;KACrC;IACD;QACE,IAAI,EAAE,yGAAyG;QAC/G,QAAQ,EAAE,mBAAmB;QAC7B,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,wDAAwD;KACtE;IACD,+EAA+E;IAC/E,8EAA8E;IAC9E,qFAAqF;IACrF;QACE,IAAI,EAAE,yHAAyH;QAC/H,QAAQ,EAAE,sBAAsB;QAChC,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,MAAM;QACf,WAAW,EAAE,+BAA+B;KAC7C;IACD;QACE,IAAI,EAAE,4IAA4I;QAClJ,QAAQ,EAAE,oBAAoB;QAC9B,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,MAAM;QACf,WAAW,EAAE,qCAAqC;KACnD;IACD,iFAAiF;IACjF,4EAA4E;IAC5E,qEAAqE;IACrE,gFAAgF;IAChF,iFAAiF;IACjF;QACE,IAAI,EAAE,qIAAqI;QAC3I,QAAQ,EAAE,wBAAwB;QAClC,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,yDAAyD;KACvE;IACD;QACE,IAAI,EAAE,6HAA6H;QACnI,QAAQ,EAAE,cAAc;QACxB,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,kEAAkE;KAChF;IACD;QACE,IAAI,EAAE,mIAAmI;QACzI,QAAQ,EAAE,iBAAiB;QAC3B,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,OAAO;QAChB,WAAW,EAAE,oCAAoC;KAClD;CACF,CAAC;AAEF,+EAA+E;AAC/E,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AAEhC,QAAA,qBAAqB,GAAG,gBAAgB,CAAC;AACzC,QAAA,wBAAwB,GAAG,6DAA6D,CAAC;AAEtG;;;;GAIG;AACH,SAAgB,kBAAkB,CAAC,IAAiD;IAClF,OAAO,IAAI,CAAC,QAAQ,KAAK,6BAAqB,IAAI,IAAI,CAAC,WAAW,KAAK,gCAAwB,CAAC;AAClG,CAAC;AAED,MAAM,YAAY,GAAuB;IACvC,QAAQ,EAAE,6BAAqB;IAC/B,6EAA6E;IAC7E,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ;IAClB,OAAO,EAAE,OAAO;IAChB,WAAW,EAAE,gCAAwB;CACtC,CAAC;AAEF,MAAM,UAAU,GAA6B,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AAE7E,iFAAiF;AACjF,+EAA+E;AAC/E,kFAAkF;AAClF,kFAAkF;AAClF,uDAAuD;AACvD,MAAM,QAAQ,GAAuC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC;AAEzG,SAAS,YAAY,CAAC,IAAc,EAAE,IAAwB,EAAE,SAAmB;IACjF,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,kBAAkB,EAAE,IAAI,CAAC,OAAO;QAChC,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACnD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,QAAQ,CAAC,IAAc;IAC9B,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,4DAA4D;IAC5D,MAAM,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,IAAI,EAAE,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,GAAG,CAAC;IACtG,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC;IAC/C,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAEhD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzC,MAAM,SAAS,GAAe,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,uCAAuC;QACvC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,SAAS;QACtC,+EAA+E;QAC/E,6EAA6E;QAC7E,2BAA2B;QAC3B,IAAI,IAAI,CAAC,QAAQ,KAAK,kBAAkB;YAAE,SAAS;QACnD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxB,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;IACtF,CAAC;IACD,OAAO,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC;AACjC,CAAC;AAED,2EAA2E;AAC3E,SAAgB,cAAc,CAAC,KAAiB;IAC9C,8EAA8E;IAC9E,6EAA6E;IAC7E,gDAAgD;IAChD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC3F,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC;IAClC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IACvE,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5F,MAAM,KAAK,GAAwB,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;IAC3F,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,MAAM,CAAC;IACjE,MAAM,OAAO,GACX,MAAM,CAAC,MAAM,KAAK,CAAC;QACjB,CAAC,CAAC,uHAAuH;QACzH,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,0BAA0B,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,oHAAoH,CAAC;IAC7M,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACnC,CAAC;AAED,kFAAkF;AAClF,SAAgB,UAAU,CAAC,KAAiB;IAC1C,MAAM,KAAK,GAAG,KAAK;SAChB,OAAO,CAAC,QAAQ,CAAC;QAClB,0EAA0E;QAC1E,+DAA+D;SAC9D,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IACnE,OAAO,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,SAAgB,sBAAsB,CACpC,MAAkB,EAClB,MAAoF;IAEpF,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACtC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3C,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACpD,MAAM,QAAQ,GACZ,MAAM,KAAK,UAAU,IAAI,MAAM,KAAK,MAAM;YACxC,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,MAAM,KAAK,QAAQ;gBACnB,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,MAAM,KAAK,KAAK;oBAChB,CAAC,CAAC,KAAK;oBACP,CAAC,CAAC,SAAS,CAAC;QACpB,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3B,MAAM,EAAE,CAAC;QACT,OAAO;YACL,GAAG,IAAI;YACP,QAAQ;YACR,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,QAAQ;YACpD,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,WAAW;SACtD,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,MAAM;QAAE,OAAO,MAAM,CAAC;IAC3B,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AACvE,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Optional LLM pass enumerating CROSS-TOOL threat chains.
3
+ *
4
+ * The deterministic engine grades one tool at a time, which is exactly what it
5
+ * can defend: a rule sees a name and a description. It cannot see that "read CRM"
6
+ * plus "send email" is an exfiltration path, because neither tool is dangerous
7
+ * alone. That combination is what this pass is for, and it is the analysis Koray
8
+ * described the hosted backend as doing.
9
+ *
10
+ * ADDITIVE ONLY. Threats never replace the deterministic paths and never move the
11
+ * risk score: the score gates CI through --fail-on, and a number that moved
12
+ * because a model phrased things differently this run would make that gate flaky.
13
+ * The dashboard rescores on threats; a terminal that exits non-zero must not.
14
+ *
15
+ * Spends the user's own key. One request, bounded, hard timeout; any failure
16
+ * returns no threats and the scan keeps its deterministic report.
17
+ */
18
+ import type { ScanTool, Severity, Verdict } from "./risk";
19
+ export interface Threat {
20
+ /** The bad OUTCOME in plain English, not a tool name. */
21
+ title: string;
22
+ /** Which capabilities combine to enable it, e.g. "CRM read + browser". */
23
+ enabledBy?: string;
24
+ category: string;
25
+ severity: Severity;
26
+ recommendedVerdict: Verdict;
27
+ consequence: string;
28
+ }
29
+ export interface ThreatOutcome {
30
+ threats: Threat[];
31
+ requests: number;
32
+ skipped: number;
33
+ error?: string;
34
+ }
35
+ export declare function deriveThreats(tools: ScanTool[], options: {
36
+ apiKey: string;
37
+ model?: string;
38
+ timeoutMs?: number;
39
+ }): Promise<ThreatOutcome>;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deriveThreats = deriveThreats;
4
+ const llm_1 = require("./llm");
5
+ /** Matches the dashboard's ceiling, so the two passes see the same surface. */
6
+ const MAX_TOOLS = 200;
7
+ const DESC_CAP = 240;
8
+ const SYSTEM_PROMPT = `You are an AI-agent security analyst. Given an agent's tools, enumerate the most consequential risk paths — including CROSS-TOOL chains (e.g. read sensitive data + external send = exfiltration).
9
+
10
+ Frame each as a THREAT a non-technical manager understands — the bad OUTCOME, not the tool name. Good titles: "Infer sensitive personal characteristics", "Leak CRM data into external tools", "Issue unauthorized or excessive refunds", "Weaken its own policy or authority". Bad titles: raw tool names like "deletePet".
11
+
12
+ Return ONLY a JSON object of the form {"threats":[ ... ]}, no prose. Each entry:
13
+ {"title":"<the threat, plain English>","enabledBy":"<which capabilities enable it, e.g. 'CRM read + browser'>","category":"<one category from the list below>","severity":"low|medium|high","recommendedVerdict":"allow|hold|block|jail","consequence":"<concrete harm in plain words>"}
14
+
15
+ "category" MUST be EXACTLY one of these protection categories (pick the closest):
16
+ Financial, Communication, File system, Code execution, Network & web, Accounts & identity, Calendar, Self-modification, Cross-agent, Behavioral anomaly.
17
+
18
+ Rules:
19
+ - recommendedVerdict: block = must never run; jail = attempts to weaken its own authority; hold = needs human approval; allow = low-consequence.
20
+ - Judge the exposure the tools ALLOW, not intent. Never invent capabilities not implied by the tools.
21
+ - Return AT MOST 6 entries, the most consequential ones. Fewer is better than padding with near-duplicates.
22
+ - Keep every field short: "title" at most 8 words, "consequence" at most 15 words, "enabledBy" at most 8 words. Do not restate the title in the consequence.`;
23
+ const SEVERITIES = ["low", "medium", "high"];
24
+ const VERDICTS = ["allow", "hold", "block", "jail"];
25
+ async function deriveThreats(tools, options) {
26
+ const batch = tools.slice(0, MAX_TOOLS);
27
+ const skipped = Math.max(0, tools.length - batch.length);
28
+ if (batch.length === 0)
29
+ return { threats: [], requests: 0, skipped: 0 };
30
+ const catalog = batch.map((t) => ({ name: t.name, description: (t.description ?? "").slice(0, DESC_CAP) }));
31
+ const res = await (0, llm_1.chatJson)({
32
+ apiKey: options.apiKey,
33
+ model: options.model,
34
+ timeoutMs: options.timeoutMs,
35
+ system: SYSTEM_PROMPT,
36
+ user: JSON.stringify(catalog),
37
+ maxTokens: 2000,
38
+ });
39
+ if (!res.data)
40
+ return { threats: [], requests: res.requests, skipped, error: res.error };
41
+ // Accept the {"threats":[...]} wrapper or a bare array, since models drop the
42
+ // wrapper often enough that rejecting it would lose whole runs.
43
+ const raw = Array.isArray(res.data)
44
+ ? res.data
45
+ : Array.isArray(res.data.threats)
46
+ ? (res.data.threats)
47
+ : [];
48
+ const threats = [];
49
+ const seen = new Set();
50
+ for (const entry of raw) {
51
+ if (!entry || typeof entry !== "object")
52
+ continue;
53
+ const p = entry;
54
+ const title = (0, llm_1.coerceStr)(p.title, 140).trim();
55
+ if (!title || seen.has(title.toLowerCase()))
56
+ continue;
57
+ seen.add(title.toLowerCase());
58
+ const severity = SEVERITIES.find((s) => s === (0, llm_1.coerceStr)(p.severity, 12).trim().toLowerCase()) ?? "medium";
59
+ const verdict = VERDICTS.find((v) => v === (0, llm_1.coerceStr)(p.recommendedVerdict, 12).trim().toLowerCase()) ?? "hold";
60
+ const enabledBy = (0, llm_1.coerceStr)(p.enabledBy, 140).trim();
61
+ threats.push({
62
+ title,
63
+ ...(enabledBy ? { enabledBy } : {}),
64
+ category: (0, llm_1.coerceStr)(p.category, 60).trim() || "Behavioral anomaly",
65
+ severity,
66
+ recommendedVerdict: verdict,
67
+ consequence: (0, llm_1.coerceStr)(p.consequence, 200).trim(),
68
+ });
69
+ }
70
+ return { threats, requests: res.requests, skipped };
71
+ }
72
+ //# sourceMappingURL=threats-llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threats-llm.js","sourceRoot":"","sources":["../../src/lib/threats-llm.ts"],"names":[],"mappings":"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"}