@interlok/cli 0.0.0-stage → 1.3.0

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 ADDED
@@ -0,0 +1,5 @@
1
+ Apache License 2.0
2
+
3
+ Copyright 2026 Interlok
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy at https://www.apache.org/licenses/LICENSE-2.0
package/README.md CHANGED
@@ -1,3 +1,159 @@
1
- # Temporary Holding Version
1
+ # Interlok CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Seven commands: `scan`, `shadow`, `proxy`, `gate`, `verify`, `approvals-mcp`, `hook`. Apache-2.0. Telemetry is off; nothing leaves your machine unless you pass `--publish`, run `shadow`, or an agent asks a person through `hook` or `approvals-mcp`.
4
+
5
+ ## scan: coverage, locally
6
+
7
+ ```bash
8
+ npx @interlok/cli scan # reads .mcp.json, .vscode/mcp.json, .cursor/mcp.json
9
+ npx @interlok/cli scan .mcp.json --enumerate # starts each server and lists its tools
10
+ npx @interlok/cli scan --ci --min-coverage 80 # exit 5 below 80% of high-risk write paths guarded
11
+ npx @interlok/cli scan --publish # signed public report + README badge (needs a key)
12
+ ```
13
+
14
+ Coverage counts only high-consequence write paths (money, access, deletion, code execution, deploys, messages to people) and how many have a guard. Tools whose names can't be classified count as high-risk until someone classifies them. `src/classify.ts` is the same file the hosted app uses; a test fails the build if they differ.
15
+
16
+ ## shadow: what your agent would have done, on every pull request
17
+
18
+ ```bash
19
+ npx @interlok/cli shadow --agent my-agent --markdown interlok.md \
20
+ --fail-on new_high_reach,coverage_drop -- npm run agent:e2e
21
+ ```
22
+
23
+ In GitHub Actions the commit, branch, and base branch are read from the environment, so a pull request is compared with the latest run on the branch it merges into. `--fail-on` takes `would_hold`, `new_high_reach`, `coverage_drop` (exit 4). The Markdown also goes to the Actions run summary. A complete workflow is in `docs/ci/interlok-shadow.yml`.
24
+
25
+ ## proxy: put any MCP server behind Interlok, no code
26
+
27
+ In your MCP client config (Claude Desktop, Cursor, VS Code, your own agent), wrap the server's command:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "github": {
33
+ "command": "npx",
34
+ "args": [
35
+ "-y",
36
+ "@interlok/cli",
37
+ "proxy",
38
+ "--name",
39
+ "github",
40
+ "--agent",
41
+ "support-bot",
42
+ "--",
43
+ "npx",
44
+ "-y",
45
+ "@modelcontextprotocol/server-github"
46
+ ],
47
+ "env": { "INTERLOK_API_KEY": "ilk_live_…", "GITHUB_PERSONAL_ACCESS_TOKEN": "…" }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ For a remote server, run `npx @interlok/cli proxy --name crm --upstream https://mcp.example.com/mcp --port 8787` and point the client at `http://127.0.0.1:8787/mcp`. Cloud agent builders (Copilot Studio, Agentforce, AgentCore Gateway) cannot reach loopback: run the proxy behind your HTTPS ingress, add `--host 0.0.0.0` inside a container, and register the ingress URL as the MCP server. The client's own credentials still go to your server; the proxy adds none and forwards none of Interlok's.
54
+
55
+ - **Gate (default):** every `tools/call` is asked of the gate first. Allowed calls run; a held call waits for a person (up to `--wait`, default 300 s) and runs only if released; a denied call never reaches the server and the model gets the reason. If Interlok can't be reached the call is refused unless you pass `--fail-open`.
56
+ - **Shadow:** under `interlok shadow -- <your test command>` (or with `--shadow <run>`), every call is recorded into the run and forwarded; nothing is blocked.
57
+ - Everything else (initialize, tools/list, resources, pings) passes through untouched. The server's tool list is registered once so description changes show up as drift.
58
+ - Your Interlok key is never passed to the MCP server, and the proxy writes only protocol messages to stdout.
59
+
60
+ ## hook: hold a coding agent's risky commands for a person
61
+
62
+ For Claude Code, Codex, Cursor, Gemini CLI and GitHub Copilot (the Copilot CLI, the Copilot cloud
63
+ agent and VS Code), including inside Paradev and Conductor workspaces. Each shell
64
+ command and MCP call is checked locally against `.interlok/policy.json`; only a match leaves the
65
+ machine, as an approval a named person answers.
66
+
67
+ ```bash
68
+ npm i -g @interlok/cli
69
+ interlok hook init [--agent claude,codex,cursor,gemini,copilot] [--npx]
70
+ export INTERLOK_API_KEY=ilk_... # approvals:write scope
71
+ interlok hook test "vercel deploy --prod" # which rule would hold it, without running anything
72
+ ```
73
+
74
+ `init` writes the policy file (built-in rules: protected-branch and force pushes, PR merges,
75
+ production deploys, infrastructure applies, migrations and destructive SQL, publishes, secret
76
+ changes, deletes outside the project, MCP tools that change something) and merges the hook into
77
+ `.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`, `.gemini/settings.json` and
78
+ `.github/hooks/interlok.json` without touching what is there. Add rules with `id`, `match` (regex on the command line), optional `tool`, `unless` and
79
+ `approvers`; set `"defaults": false` to use only your own.
80
+
81
+ - Approvals are single-use: the same command later asks again.
82
+ - Still held after `wait` seconds: the agent is told to continue other work and retry the exact
83
+ command; the retry resolves the same request instead of opening a new one.
84
+ - Fails closed with no key, no connection or an unreadable policy; `--fail-open` relaxes only an
85
+ unreachable Interlok.
86
+ - Credentials in a command line are masked before anyone sees it; `"summary": "hash-only"` sends
87
+ only the rule and a hash.
88
+ - Gemini CLI and Copilot let a call run when a hook times out, so every config gives the hook 120
89
+ seconds and the wait for a person is capped at 100.
90
+ - Copilot: commit `.github/hooks/interlok.json` (the cloud agent reads only that folder) and put
91
+ `INTERLOK_API_KEY` in the repository's `copilot` environment secrets. Copilot also runs the hooks
92
+ in `.claude/settings.json`; with both files present, the Claude entry steps aside for Copilot so
93
+ one command opens one request.
94
+
95
+ ## approvals-mcp: let any MCP agent ask a person first
96
+
97
+ One tool, `request_human_approval`, for agents that should check with a person before they act. No gateway and no policy: the request always waits, a named person approves or denies it in the Interlok console, and the agent gets the answer with a signed receipt. Only `approved` means go ahead; anything else, including Interlok being unreachable, tells the agent not to act.
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "interlok-approvals": {
103
+ "command": "npx",
104
+ "args": ["-y", "@interlok/cli", "approvals-mcp", "--agent", "support-bot"],
105
+ "env": { "INTERLOK_API_KEY": "ilk_live_…" }
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ - The `payload` argument is hashed on your machine; only its SHA-256 reaches Interlok.
112
+ - `--wait <seconds>` (default 120) is how long the tool waits for a person before answering "still waiting".
113
+ - The key needs the `approvals:write` and `approvals:read` scopes. A key can ask; it can never answer.
114
+
115
+ ## gate: hold a risky deploy for a person
116
+
117
+ ```yaml
118
+ jobs:
119
+ gate:
120
+ permissions: { id-token: write, contents: read }
121
+ steps:
122
+ - uses: actions/checkout@v4
123
+ with: { fetch-depth: 0 }
124
+ - run: terraform show -json tfplan > plan.json # optional
125
+ - run: npx @interlok/cli gate --action deploy --env production --plan plan.json --wait 60m
126
+ env: { INTERLOK_CONNECTOR: <connector id from Console → Settings → Connectors> }
127
+ deploy:
128
+ needs: gate
129
+ steps:
130
+ # ... deploy ...
131
+ - if: always()
132
+ run: npx @interlok/cli gate report --status ${{ job.status }}
133
+ ```
134
+
135
+ CI authenticates with its OIDC token; nothing is stored. The plan is summarized locally and only the summary and its hash are sent. Low-risk changes pass in seconds; a held change waits for a person in Slack or the console. `--emergency --reason "..."` is break-glass, allowed only for people on the connector's emergency list and never for agent- or bot-authored changes; a second person reviews within 24 h. Protect the workflow file itself (CODEOWNERS and branch protection): a CI gate is only as strong as the workflow that calls it. Exit codes: 0 allowed or released · 10 denied · 11 hold expired · 12 Interlok unreachable (fails closed unless `--fail-open`) · 13 config or auth error.
136
+
137
+ ### Model promotions
138
+
139
+ The same step holds a production model change for a person: a provider swap, new weights, a floating alias (`latest`, `preview`), a failed or missing eval, or a model that serves governed data.
140
+
141
+ ```bash
142
+ npx @interlok/cli gate --action model.promote --env production \
143
+ --provider-from openai --provider-to aws.bedrock \
144
+ --model-from gpt-4.1-2025-04-14 --model-to anthropic.claude-sonnet-4-5-20250929-v1:0 \
145
+ --model-change provider,model --eval-suite regression-v3 --eval-passed true \
146
+ --classification cui
147
+ ```
148
+
149
+ `--model-change` takes any of `model, provider, weights, fine_tune, quantization, prompt_template, decoding, alias`. `--digest-from/--digest-to` take the sha256 of weights or an image when your platform can state one.
150
+
151
+ ## verify: offline, no account
152
+
153
+ ```bash
154
+ npx @interlok/cli verify interlok-shadow-<run>.json
155
+ ```
156
+
157
+ Checks every signature and that the events, coverage, and run diff are exactly what was signed. Public reports (`/r/<slug>`) verify the same way.
158
+
159
+ Exit codes: 1 error · 2 critical finding · 3 verification failed · 4 `--fail-on` tripped · 5 below `--min-coverage`.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `interlok approvals-mcp`: an MCP server with one tool, `request_human_approval`. Any MCP agent
3
+ * (Claude, Cursor, Copilot, your own) can ask a named person before it does something it should
4
+ * not do alone. The request waits in the Interlok console approvals queue (or a connected channel); only a
5
+ * person can answer it, and the answer comes back with a signed receipt.
6
+ *
7
+ * { "mcpServers": { "interlok-approvals": { "command": "npx",
8
+ * "args": ["-y", "@interlok/cli", "approvals-mcp"],
9
+ * "env": { "INTERLOK_API_KEY": "ilk_..." } } } }
10
+ *
11
+ * The payload is hashed here and never sent. Protocol messages go to stdout; everything else to
12
+ * stderr. Nothing in this file can approve: the API refuses determinations from a key.
13
+ */
14
+ export declare const TOOL: {
15
+ readonly name: "request_human_approval";
16
+ readonly description: "Ask a named person to approve an action before you take it. Use this before anything irreversible or consequential: paying, refunding, deleting, deploying, emailing customers, changing permissions. Wait for the answer and act only if it is approved.";
17
+ readonly inputSchema: {
18
+ readonly type: "object";
19
+ readonly properties: {
20
+ readonly action: {
21
+ readonly type: "string";
22
+ readonly description: "Short machine name of what you want to do, e.g. refund.issue or db.drop_table.";
23
+ };
24
+ readonly summary: {
25
+ readonly type: "string";
26
+ readonly description: "One or two sentences the approver reads: what, to whom, how much, and why.";
27
+ };
28
+ readonly payload: {
29
+ readonly description: "The exact request you will send if approved. Only its SHA-256 leaves this machine.";
30
+ };
31
+ readonly context: {
32
+ readonly type: "object";
33
+ readonly description: "Up to 20 short fields shown to the approver (strings, numbers, booleans).";
34
+ readonly additionalProperties: {
35
+ readonly type: readonly ["string", "number", "boolean"];
36
+ };
37
+ };
38
+ };
39
+ readonly required: readonly ["action", "summary"];
40
+ };
41
+ };
42
+ export interface McpConfig {
43
+ api: string;
44
+ key: string;
45
+ agent: string;
46
+ /** How long to wait for a person before answering "still waiting". */
47
+ waitSeconds: number;
48
+ fetch?: typeof fetch;
49
+ sleep?: (ms: number) => Promise<void>;
50
+ log?: (line: string) => void;
51
+ }
52
+ type Msg = {
53
+ jsonrpc: "2.0";
54
+ id?: string | number | null;
55
+ method?: string;
56
+ params?: any;
57
+ };
58
+ export declare const payloadHash: (payload: unknown) => string;
59
+ /** What the model reads for each outcome. Only "approved" means go ahead. */
60
+ export declare function describe(a: {
61
+ status: string;
62
+ hold?: {
63
+ tag_number?: string | null;
64
+ };
65
+ determinations?: {
66
+ by: string;
67
+ note: string | null;
68
+ }[];
69
+ receipt?: {
70
+ entries?: {
71
+ seq: number;
72
+ entry_hash: string;
73
+ }[];
74
+ };
75
+ console_url?: string;
76
+ }): {
77
+ text: string;
78
+ isError: boolean;
79
+ };
80
+ /** Handle one JSON-RPC message. Returns the reply, or null for notifications. */
81
+ export declare function handle(m: Msg, cfg: McpConfig): Promise<object | null>;
82
+ export declare function approvalsMcpCommand(value: (flag: string) => string | undefined): Promise<void>;
83
+ export {};
@@ -0,0 +1,185 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createInterface } from "node:readline";
3
+ import { canonical } from "./canonical.js";
4
+ /**
5
+ * `interlok approvals-mcp`: an MCP server with one tool, `request_human_approval`. Any MCP agent
6
+ * (Claude, Cursor, Copilot, your own) can ask a named person before it does something it should
7
+ * not do alone. The request waits in the Interlok console approvals queue (or a connected channel); only a
8
+ * person can answer it, and the answer comes back with a signed receipt.
9
+ *
10
+ * { "mcpServers": { "interlok-approvals": { "command": "npx",
11
+ * "args": ["-y", "@interlok/cli", "approvals-mcp"],
12
+ * "env": { "INTERLOK_API_KEY": "ilk_..." } } } }
13
+ *
14
+ * The payload is hashed here and never sent. Protocol messages go to stdout; everything else to
15
+ * stderr. Nothing in this file can approve: the API refuses determinations from a key.
16
+ */
17
+ export const TOOL = {
18
+ name: "request_human_approval",
19
+ description: "Ask a named person to approve an action before you take it. Use this before anything irreversible or consequential: paying, refunding, deleting, deploying, emailing customers, changing permissions. Wait for the answer and act only if it is approved.",
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ action: {
24
+ type: "string",
25
+ description: "Short machine name of what you want to do, e.g. refund.issue or db.drop_table.",
26
+ },
27
+ summary: {
28
+ type: "string",
29
+ description: "One or two sentences the approver reads: what, to whom, how much, and why.",
30
+ },
31
+ payload: {
32
+ description: "The exact request you will send if approved. Only its SHA-256 leaves this machine.",
33
+ },
34
+ context: {
35
+ type: "object",
36
+ description: "Up to 20 short fields shown to the approver (strings, numbers, booleans).",
37
+ additionalProperties: { type: ["string", "number", "boolean"] },
38
+ },
39
+ },
40
+ required: ["action", "summary"],
41
+ },
42
+ };
43
+ export const payloadHash = (payload) => createHash("sha256").update(canonical(payload)).digest("hex");
44
+ const text = (id, t, isError = false) => ({
45
+ jsonrpc: "2.0",
46
+ id,
47
+ result: { ...(isError ? { isError: true } : {}), content: [{ type: "text", text: t }] },
48
+ });
49
+ /** What the model reads for each outcome. Only "approved" means go ahead. */
50
+ export function describe(a) {
51
+ const tag = a.hold?.tag_number ? ` (tag ${a.hold.tag_number})` : "";
52
+ const who = (a.determinations ?? []).map((d) => d.by).join(", ");
53
+ const note = (a.determinations ?? []).find((d) => d.note)?.note;
54
+ const last = a.receipt?.entries?.at(-1);
55
+ const receipt = last ? ` Signed receipt: ledger entry #${last.seq} (${last.entry_hash}).` : "";
56
+ switch (a.status) {
57
+ case "approved":
58
+ return {
59
+ text: `APPROVED by ${who || "a person"}${tag}. You may proceed with exactly this request.${note ? ` Note: ${note}` : ""}${receipt}`,
60
+ isError: false,
61
+ };
62
+ case "denied":
63
+ return {
64
+ text: `DENIED by ${who || "a person"}${tag}. Do not take this action.${note ? ` Reason: ${note}` : ""}${receipt}`,
65
+ isError: true,
66
+ };
67
+ case "expired":
68
+ case "cancelled":
69
+ return {
70
+ text: `No approval${tag}: the request ${a.status}. Do not take this action.`,
71
+ isError: true,
72
+ };
73
+ default:
74
+ return {
75
+ text: `Still waiting for a person${tag}. Do not take this action yet.${a.console_url ? ` Approvers decide at ${a.console_url}.` : ""}`,
76
+ isError: true,
77
+ };
78
+ }
79
+ }
80
+ async function call(cfg, path, init = {}) {
81
+ const r = await (cfg.fetch ?? fetch)(`${cfg.api}${path}`, {
82
+ ...init,
83
+ headers: {
84
+ "content-type": "application/json",
85
+ authorization: `Bearer ${cfg.key}`,
86
+ ...(init.headers ?? {}),
87
+ },
88
+ signal: AbortSignal.timeout(30_000),
89
+ });
90
+ const body = await r.json().catch(() => ({}));
91
+ if (!r.ok)
92
+ throw new Error(`Interlok ${r.status}: ${body?.error ?? "request failed"}`);
93
+ return body;
94
+ }
95
+ /** Handle one JSON-RPC message. Returns the reply, or null for notifications. */
96
+ export async function handle(m, cfg) {
97
+ if (m.id === undefined || m.id === null)
98
+ return null; // notifications need no answer
99
+ switch (m.method) {
100
+ case "initialize":
101
+ return {
102
+ jsonrpc: "2.0",
103
+ id: m.id,
104
+ result: {
105
+ protocolVersion: m.params?.protocolVersion ?? "2025-06-18",
106
+ capabilities: { tools: {} },
107
+ serverInfo: { name: "interlok-approvals", version: "1.0.0" },
108
+ },
109
+ };
110
+ case "ping":
111
+ return { jsonrpc: "2.0", id: m.id, result: {} };
112
+ case "tools/list":
113
+ return { jsonrpc: "2.0", id: m.id, result: { tools: [TOOL] } };
114
+ case "tools/call": {
115
+ if (m.params?.name !== TOOL.name)
116
+ return text(m.id, `Unknown tool ${m.params?.name}`, true);
117
+ const a = m.params?.arguments ?? {};
118
+ if (typeof a.action !== "string" || typeof a.summary !== "string")
119
+ return text(m.id, "request_human_approval needs an action and a summary.", true);
120
+ try {
121
+ const created = await call(cfg, "/api/v1/approvals", {
122
+ method: "POST",
123
+ body: JSON.stringify({
124
+ action: a.action,
125
+ summary: a.summary,
126
+ payload_hash: a.payload !== undefined ? payloadHash(a.payload) : undefined,
127
+ context: a.context,
128
+ agent: { name: cfg.agent },
129
+ }),
130
+ });
131
+ let current = created;
132
+ const sleep = cfg.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
133
+ const deadline = Date.now() + cfg.waitSeconds * 1000;
134
+ while (current.status === "held" && Date.now() < deadline) {
135
+ await sleep(2000);
136
+ current = await call(cfg, `/api/v1/approvals/${encodeURIComponent(created.id)}`);
137
+ }
138
+ const d = describe(current);
139
+ return text(m.id, d.text, d.isError);
140
+ }
141
+ catch (e) {
142
+ cfg.log?.(`approval request failed: ${e.message}`);
143
+ // Fail closed: without an answer from a person, the action does not happen.
144
+ return text(m.id, `Could not reach Interlok (${e.message}). No approval: do not take this action.`, true);
145
+ }
146
+ }
147
+ default:
148
+ return {
149
+ jsonrpc: "2.0",
150
+ id: m.id,
151
+ error: { code: -32601, message: `Unknown method ${m.method}` },
152
+ };
153
+ }
154
+ }
155
+ export async function approvalsMcpCommand(value) {
156
+ const cfg = {
157
+ api: (value("--api") ?? process.env.INTERLOK_URL ?? "https://interlok.io").replace(/\/$/, ""),
158
+ key: process.env.INTERLOK_API_KEY ?? "",
159
+ agent: value("--agent") ?? process.env.INTERLOK_AGENT ?? "mcp-agent",
160
+ waitSeconds: Number(value("--wait") ?? 120),
161
+ log: (l) => process.stderr.write(`[interlok] ${l}\n`),
162
+ };
163
+ if (!cfg.key) {
164
+ process.stderr.write("interlok approvals-mcp needs INTERLOK_API_KEY (approvals:write scope)\n");
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+ const rl = createInterface({ input: process.stdin });
169
+ for await (const line of rl) {
170
+ if (!line.trim())
171
+ continue;
172
+ let m;
173
+ try {
174
+ m = JSON.parse(line);
175
+ }
176
+ catch {
177
+ continue;
178
+ }
179
+ // Each message on its own: a request waiting on a person never blocks pings or other calls.
180
+ void handle(m, cfg).then((reply) => {
181
+ if (reply)
182
+ process.stdout.write(JSON.stringify(reply) + "\n");
183
+ });
184
+ }
185
+ }
@@ -0,0 +1 @@
1
+ export declare function canonical(value: unknown): string;
@@ -0,0 +1,20 @@
1
+ export function canonical(value) {
2
+ if (value === null || typeof value === "boolean" || typeof value === "string")
3
+ return JSON.stringify(value);
4
+ if (typeof value === "number") {
5
+ if (!Number.isFinite(value))
6
+ throw new Error("non-finite number");
7
+ return JSON.stringify(value);
8
+ }
9
+ if (Array.isArray(value))
10
+ return `[${value.map(canonical).join(",")}]`;
11
+ if (typeof value === "object") {
12
+ const o = value;
13
+ return `{${Object.keys(o)
14
+ .filter((k) => o[k] !== undefined)
15
+ .sort()
16
+ .map((k) => `${JSON.stringify(k)}:${canonical(o[k])}`)
17
+ .join(",")}}`;
18
+ }
19
+ throw new Error(`unsupported type ${typeof value}`);
20
+ }
@@ -0,0 +1,107 @@
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 declare const CLASSIFIER_VERSION = "1.0.0";
20
+ export type RiskTier = "low" | "medium" | "high" | "critical";
21
+ export type Reversibility = "reversible" | "compensable" | "irreversible" | "unknown";
22
+ export type SideEffect = "payment" | "access_grant" | "destructive" | "code_exec" | "deploy" | "outbound_msg" | "external_write" | "data_write";
23
+ export type DataClass = "financial" | "pii" | "secrets" | "prod_data" | "comms";
24
+ export type ProfileSource = "declared" | "registry" | "annotation" | "heuristic" | "unknown";
25
+ /** The MCP tool annotations (spec 2025-03-26+). Hints from the server, never trusted to lower risk. */
26
+ export interface ToolAnnotations {
27
+ title?: string;
28
+ readOnlyHint?: boolean;
29
+ destructiveHint?: boolean;
30
+ idempotentHint?: boolean;
31
+ openWorldHint?: boolean;
32
+ }
33
+ export interface ToolDescriptor {
34
+ name: string;
35
+ description?: string | null;
36
+ input_schema?: unknown;
37
+ annotations?: ToolAnnotations | null;
38
+ /** Registry value. Present when the tool was registered against a connection. */
39
+ write_path?: boolean | null;
40
+ }
41
+ /** What a person can pin for a tool. Every field optional; anything set beats the heuristics. */
42
+ export interface DeclaredProfile {
43
+ write_path?: boolean;
44
+ risk_tier?: RiskTier;
45
+ reversibility?: Reversibility;
46
+ data_classes?: DataClass[];
47
+ side_effects?: SideEffect[];
48
+ }
49
+ export interface ToolProfile {
50
+ tool: string;
51
+ write_path: boolean;
52
+ risk_tier: RiskTier;
53
+ reversibility: Reversibility;
54
+ data_classes: DataClass[];
55
+ side_effects: SideEffect[];
56
+ source: ProfileSource;
57
+ /** True when no verb matched and the profile is a fail-closed default. */
58
+ unclassified: boolean;
59
+ /** Human-readable reasons, in the order they were applied. */
60
+ basis: string[];
61
+ classifier_version: string;
62
+ }
63
+ export declare const tierRank: (t: RiskTier) => number;
64
+ export declare const maxTier: (a: RiskTier, b: RiskTier) => RiskTier;
65
+ export declare const atLeast: (t: RiskTier, floor: RiskTier) => boolean;
66
+ /** The verb part of a namespaced or dotted tool name: `github/create_release` → `create_release`. */
67
+ export declare function bareToolName(tool: string): string;
68
+ export declare function classifyTool(t: ToolDescriptor, declared?: DeclaredProfile | null): ToolProfile;
69
+ export type CoverageRung = "guarded" | "exercised" | "governed" | "enforced";
70
+ export interface CoverageItem {
71
+ /** `server/tool` or `connection/tool`. */
72
+ key: string;
73
+ profile: ToolProfile;
74
+ guarded?: boolean;
75
+ exercised?: boolean;
76
+ governed?: boolean;
77
+ enforced?: boolean;
78
+ }
79
+ export interface CoverageRungResult {
80
+ covered: number;
81
+ total: number;
82
+ /** null when there is nothing to cover: no high-risk write paths at all. */
83
+ pct: number | null;
84
+ }
85
+ export interface CoveragePath {
86
+ key: string;
87
+ risk_tier: RiskTier;
88
+ unclassified: boolean;
89
+ guarded?: boolean;
90
+ exercised?: boolean;
91
+ governed?: boolean;
92
+ enforced?: boolean;
93
+ }
94
+ export interface Coverage {
95
+ classifier_version: string;
96
+ threshold: RiskTier;
97
+ /** High-consequence write paths: the denominator for every rung. */
98
+ high_risk_write_paths: number;
99
+ /** How many of those are fail-closed defaults nobody has classified yet. */
100
+ unclassified: number;
101
+ rungs: Partial<Record<CoverageRung, CoverageRungResult>>;
102
+ /** The paths themselves, sorted by key so the list is stable. */
103
+ paths: CoveragePath[];
104
+ }
105
+ export declare function computeCoverage(items: CoverageItem[], threshold?: RiskTier): Coverage;
106
+ /** One line a person can read: "guarded 3/8 (38%) · 2 unclassified". */
107
+ export declare function coverageLine(c: Coverage, rung?: CoverageRung): string;