@atbash/cli 0.5.15-dev.8 → 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 (58) hide show
  1. package/README.md +287 -26
  2. package/dist/bin/atbash.js +31 -5
  3. package/dist/bin/atbash.js.map +1 -1
  4. package/dist/commands/connect.d.ts +40 -50
  5. package/dist/commands/connect.js +60 -108
  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 +1 -209
  25. package/dist/commands/setup.js +128 -947
  26. package/dist/commands/setup.js.map +1 -1
  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 +4 -4
  53. package/dist/commands/mcp-cmd.d.ts +0 -13
  54. package/dist/commands/mcp-cmd.js +0 -216
  55. package/dist/commands/mcp-cmd.js.map +0 -1
  56. package/dist/shared/atbash-targets.d.ts +0 -49
  57. package/dist/shared/atbash-targets.js +0 -63
  58. package/dist/shared/atbash-targets.js.map +0 -1
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * Shared OpenRouter transport for the scan's optional LLM passes.
4
+ *
5
+ * Every pass here spends the USER'S OWN key. The hosted dashboard runs equivalent
6
+ * passes against a platform key; running locally there is no platform key, so the
7
+ * user supplies one or the scan stays deterministic.
8
+ *
9
+ * Deliberately small: one request per pass, a hard timeout, and no connection
10
+ * pooling. The dashboard's transport multiplexes waves of batches over an HTTP/2
11
+ * pool because it grades 87-tool surfaces from a server; a terminal scan issues a
12
+ * handful of calls and does not need any of that machinery.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DEFAULT_MODEL = void 0;
16
+ exports.chatJson = chatJson;
17
+ exports.coerceStr = coerceStr;
18
+ /**
19
+ * Measured default from the dashboard's enrichment pass. Overridable with
20
+ * --model; re-measure before repointing, because the tuning below was tuned
21
+ * against this model.
22
+ */
23
+ exports.DEFAULT_MODEL = "z-ai/glm-5.3-flash";
24
+ const OPENROUTER_ENDPOINT = "https://openrouter.ai/api/v1/chat/completions";
25
+ /**
26
+ * OpenRouter settings that are load-bearing, not cosmetic.
27
+ *
28
+ * `reasoning.effort: "low"` — the default scan model is a reasoning model and
29
+ * otherwise spends ~1000 tokens per call on deliberation we never read, which we
30
+ * pay for twice: in latency, and in max_tokens, which COUNTS reasoning tokens, so
31
+ * the JSON gets truncated mid-object and the whole pass is lost.
32
+ * `provider.sort: "throughput"` — routing spread for the same model and prompt was
33
+ * measured between 9.8s and 64s. Verified harmless for non-reasoning models.
34
+ */
35
+ const REQUEST_TUNING = {
36
+ reasoning: { effort: "low" },
37
+ provider: { sort: "throughput", allow_fallbacks: true },
38
+ };
39
+ /** Strip a ```json fence if the model wrapped its answer, which not every
40
+ * provider avoids even with response_format set. */
41
+ function unfence(raw) {
42
+ const t = raw.trim();
43
+ if (!t.startsWith("```"))
44
+ return t;
45
+ return t.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "").trim();
46
+ }
47
+ async function chatJson(options) {
48
+ const ctrl = new AbortController();
49
+ // Never unbounded: a hung socket must not hold the terminal open.
50
+ const timer = setTimeout(() => ctrl.abort(), options.timeoutMs ?? 30000);
51
+ try {
52
+ const res = await fetch(OPENROUTER_ENDPOINT, {
53
+ method: "POST",
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ Authorization: `Bearer ${options.apiKey}`,
57
+ "HTTP-Referer": "https://atbash.ai",
58
+ "X-Title": "ATBASH Local Scan",
59
+ },
60
+ body: JSON.stringify({
61
+ model: options.model || exports.DEFAULT_MODEL,
62
+ messages: [
63
+ { role: "system", content: options.system },
64
+ { role: "user", content: options.user },
65
+ ],
66
+ temperature: 0,
67
+ max_tokens: options.maxTokens ?? 4000,
68
+ response_format: { type: "json_object" },
69
+ ...REQUEST_TUNING,
70
+ }),
71
+ signal: ctrl.signal,
72
+ redirect: "error",
73
+ });
74
+ if (!res.ok) {
75
+ // Status only. A provider error body can echo the request, and the request
76
+ // carries the user's key in a header.
77
+ return { data: null, requests: 1, error: `provider returned ${res.status}` };
78
+ }
79
+ const body = (await res.json());
80
+ const content = body.choices?.[0]?.message?.content;
81
+ if (!content)
82
+ return { data: null, requests: 1, error: "provider returned no content" };
83
+ return { data: JSON.parse(unfence(content)), requests: 1 };
84
+ }
85
+ catch (e) {
86
+ const msg = e instanceof Error ? e.message : String(e);
87
+ return {
88
+ data: null,
89
+ requests: 1,
90
+ // An abort is our own deadline, not a provider fault — say which.
91
+ error: ctrl.signal.aborted ? "timed out" : msg,
92
+ };
93
+ }
94
+ finally {
95
+ clearTimeout(timer);
96
+ }
97
+ }
98
+ function coerceStr(v, cap) {
99
+ return typeof v === "string" ? v.slice(0, cap) : "";
100
+ }
101
+ //# sourceMappingURL=llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/lib/llm.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AA0CH,4BAyDC;AAED,8BAEC;AArGD;;;;GAIG;AACU,QAAA,aAAa,GAAG,oBAAoB,CAAC;AAElD,MAAM,mBAAmB,GAAG,+CAA+C,CAAC;AAE5E;;;;;;;;;GASG;AACH,MAAM,cAAc,GAAG;IACrB,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;IAC5B,QAAQ,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,eAAe,EAAE,IAAI,EAAE;CAC/C,CAAC;AAUX;qDACqD;AACrD,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACrB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACnC,OAAO,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAC1E,CAAC;AAEM,KAAK,UAAU,QAAQ,CAAC,OAO9B;IACC,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAC;IACnC,kEAAkE;IAClE,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,SAAS,IAAI,KAAM,CAAC,CAAC;IAC1E,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;YAC3C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,OAAO,CAAC,MAAM,EAAE;gBACzC,cAAc,EAAE,mBAAmB;gBACnC,SAAS,EAAE,mBAAmB;aAC/B;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,qBAAa;gBACrC,QAAQ,EAAE;oBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE;oBAC3C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE;iBACxC;gBACD,WAAW,EAAE,CAAC;gBACd,UAAU,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI;gBACrC,eAAe,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;gBACxC,GAAG,cAAc;aAClB,CAAC;YACF,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,2EAA2E;YAC3E,sCAAsC;YACtC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,qBAAqB,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;QAC/E,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4D,CAAC;QAC3F,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;QACpD,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC;QAExF,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAA4B,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IACxF,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACvD,OAAO;YACL,IAAI,EAAE,IAAI;YACV,QAAQ,EAAE,CAAC;YACX,kEAAkE;YAClE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG;SAC/C,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,SAAgB,SAAS,CAAC,CAAU,EAAE,GAAW;IAC/C,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACtD,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Scan → starter policy.
3
+ *
4
+ * Turns the risk report into a first-draft governance boundary: a mandate line,
5
+ * Red Lines (block/jail paths), Approval boundaries (hold paths), and always the
6
+ * self-modification containment rule. This is what makes a scan ACTIONABLE rather
7
+ * than a read-only inventory. Deterministic, grouped by category, and needs no
8
+ * API key.
9
+ *
10
+ * PROVENANCE: ported from the dashboard's `src/lib/scan/policy-suggest.ts`.
11
+ * Suggesting a boundary is not the same as deploying one — deploying stays a
12
+ * dashboard function, and this command never writes a policy anywhere.
13
+ *
14
+ * Divergences from the dashboard version, all found against real output:
15
+ * - Takes the agent's declared instructions directly instead of a
16
+ * `CapabilityManifest`, because the CLI has no manifest type.
17
+ * - A TOOL that already carries a red line elsewhere in the same category is
18
+ * dropped from that category's approvals, not the whole category. Upstream
19
+ * emits both unconditionally, producing "never perform data access / egress"
20
+ * and "require human approval before data access / egress" for the SAME tool
21
+ * in one boundary. An earlier version of this fix dropped the whole category
22
+ * instead of just that tool — which silently discarded a DIFFERENT tool's
23
+ * real, distinct hold finding whenever it shared a category name with
24
+ * anything blocked (found against a real `export_crm_contacts` +
25
+ * `delete_customer_records` surface: the export tool's approval line vanished
26
+ * entirely, with no trace of it anywhere in the printed boundary). The
27
+ * collision check itself must key on source+name, not name alone — otherwise
28
+ * a blocked `x` on one MCP server can wrongly erase a held `x` on another.
29
+ * - Drops the appended word "actions", which doubled up on every category that
30
+ * already ended in one ("destructive action actions").
31
+ * - Prefers primary paths when picking example tool names.
32
+ */
33
+ import { type RiskReport } from "./risk";
34
+ export interface PolicySuggestion {
35
+ mandate: string;
36
+ redLines: string[];
37
+ approvals: string[];
38
+ notes: string;
39
+ }
40
+ export declare function suggestPolicy(risk: RiskReport, systemPrompt?: string): PolicySuggestion;
@@ -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