/** * Tool inventory + allowlist compilation (task 137). * * WHY THIS EXISTS * A thread that should only be able to do a few things (a public web-app visitor * panel) previously had to be configured with a DENYLIST — @cdda hand-enumerated 47 * tool names to get a safe surface. That inverts maintenance onto every app author: * any tool cumulus or the Claude CLI adds in a future release is silently granted to * every visitor on every deployed app until each author notices and edits their list. * * The fix is an allowlist. But `--allowedTools` on the Claude CLI is an AUTO-APPROVE * list, not a restriction — measured under three permission modes, `--allowedTools Glob` * failed to block Bash every time, and cumulus spawns with `--permission-mode * bypassPermissions` where an auto-approve list is a guaranteed no-op. `--disallowedTools` * DOES restrict there (also measured). So an allowlist has to be COMPILED into a denylist: * * denylist = (everything that exists) − (what this thread may use) * * which is why this module has to know the full inventory. * * WHERE THE INVENTORY COMES FROM (three sources, because no single one is complete) * 1. Built-in CLI tools — read from the `system:init` stream event, which carries the * CLI's own `tools[]`. Probed once per process (see probeBuiltinTools). * 2. cumulus's own MCP tools — derived from the definitions cumulus itself ships. * 3. Namespace `extraMcpServers` — NOT knowable here, and deliberately not closed over. * Those are the app author's own shim tools, granted to that namespace on purpose. * * The seed list is a FLOOR, not merely a fallback: the inventory is always the union of * the probe and the seed, so a probe that returns a short list (different cwd, different * plugin set, a future CLI that changes the event shape) can never quietly widen what a * constrained thread is allowed to reach. */ /** * Built-in Claude CLI tools. * * This is a FLOOR for the inventory, not a fallback — see the module comment. Adding a * name here can only ever deny more on an allowlisted thread; it has no effect on a * thread without `allowedTools`. Safe to update from a probe of a newer CLI. * * TWO CLASSES OF NAME LIVE HERE, and the second is why the seed cannot be replaced by * the probe (@pursuit, 2026-08-15): * * 1. Names `system:init` advertises in `tools[]` (29 on CLI 2.1.217). The probe finds * these, so for this class the seed is merely a floor against a short probe. * * 2. Names the CLI's PERMISSION system recognizes but `system:init` does NOT advertise * (`ATTACHED_TOOL_NAMES` below). The probe is structurally blind to these, so the * seed is the ONLY thing that puts them in the inventory — and the inventory is what * the allowlist is subtracted from. Without them, an allowlisted thread would leave * them ungoverned no matter what the author allowed. * * Measured on CLI 2.1.217: a deny rule for an unrecognized name prints `Permission deny * rule "X" matches no known tool`; every name in class 2 is accepted silently, so the CLI * does know them. `Grep`/`Glob` are additionally NOT callable on this version (a real * turn asked for `Grep`, `ToolSearch select:Grep` returned nothing, and the model fell * back to `Bash`) — they are listed as insurance against a CLI that restores them, not * because they are reachable today. */ export declare const ADVERTISED_BUILTIN_TOOLS: readonly string[]; /** * Class 2 (see SEED_BUILTIN_TOOLS): names the CLI's permission system accepts as deny * rules but which `system:init` does not list in `tools[]`, so `probeBuiltinTools` can * never discover them. Verified name-by-name against CLI 2.1.217 by checking for the * "matches no known tool" warning; `MultiEdit`, `SlashCommand`, `NotebookRead` and `LS` * were rejected and are therefore deliberately absent. * * `Agent` is the display name for the `Task` permission — denying `Task` already removes * the subagent tool (measured: the model reported "the Agent tool is unavailable"), so * this entry is redundant reinforcement, not the thing that closes that path. */ export declare const ATTACHED_TOOL_NAMES: readonly string[]; /** The inventory floor: everything the CLI advertises, plus everything it merely accepts. */ export declare const SEED_BUILTIN_TOOLS: readonly string[]; /** * Tools served by the `gateway-agents` MCP server. * * Duplicated here rather than imported because `gateway-agents-mcp.ts` calls `main()` * at top level — it is a stdio binary, and importing it would start a server. A source * tripwire test asserts this list matches the tool definitions in that file, so the two * cannot drift (same shape as task 132's artifact tripwire). * * `broadcast` is absent on purpose: it is deliberately not served (task 135). */ export declare const GATEWAY_AGENTS_TOOL_NAMES: readonly string[]; /** Fully-qualified names of the MCP tools cumulus ships itself. */ export declare function cumulusMcpToolNames(): string[]; /** * Ask the Claude CLI what built-in tools it has, by reading the `tools[]` array off its * `system:init` stream event and killing the subprocess immediately. * * Measured cost: init arrives ~871ms in and is the ONLY event before the kill — no * assistant output, no result event, so nothing comes back from the model. A prompt is * required: with stdin closed the CLI exits before emitting init (0 lines), and merely * holding stdin open emits nothing at all (20s). Hence the one-character `x`. * * Runs in a neutral cwd so a project's own settings cannot shape the answer, and the * result is unioned with the seed floor by the caller either way. * * Rejects on timeout, spawn failure, or a missing/empty `tools[]`. Never throws * synchronously; the caller treats any rejection as "use the floor". * * `claudePath` is supplied by the caller, which already has it resolved. Taking it as a * parameter rather than importing `resolveClaudeCli` avoids a module cycle: that function * lives in gateway.ts, which imports this module. */ export declare function probeBuiltinTools(claudePath?: string): Promise; /** * The full set of tool names a spawned turn could otherwise reach. * * Deliberately NOT threaded through `MessagePipelineOptions`: task 113 is the recorded * case of a field added to one of four spawn paths and silently doing nothing in the * other three, and here a miss would fail OPEN. Module-scope memoization means the single * args-assembly site is the only consumer and it cannot be half-wired. */ export declare function getToolInventory(claudePath?: string): Promise; /** Test seam: drop the memo so a suite can probe with a stub. */ export declare function resetToolInventoryCache(): void; /** The bare tool name of an `mcp__server__tool` entry, or the name itself. */ export declare function bareToolName(name: string): string; /** * Compile an allowlist into the denylist the CLI actually honours. * * Returns `existingDisallowed` UNCHANGED when no allowlist is configured — additive, so * every thread that does not opt in behaves exactly as before. * * An allowlist entry matches an inventory entry exactly, or as its bare name, so * `read_file` allows `mcp__cumulus-history__read_file` (the system prompt calls it * `read_file`; a user should not have to know the prefix). An entry that matches nothing * is inert — and inertness DENIES, so a typo fails closed. * * `disallowedTools` still applies on top: it can subtract from an allowlist but never add. */ export declare function compileDisallowedTools(allowedTools: string[] | undefined, inventory: readonly string[], existingDisallowed: string[] | undefined): string[] | undefined; /** Tool-availability view of a thread's config, used for both gating and seeding. */ export interface ToolPolicy { allowedTools?: string[]; disallowedTools?: string[]; } /** * Is `name` (a bare tool name) reachable on this thread? * * Deliberately inventory-INDEPENDENT: it reads the thread's stated intent, so it gives the * same answer on the Claude CLI path and the direct-provider path, whose real inventories * differ. Available iff the allowlist admits it (or there is no allowlist) and the denylist * does not name it. */ export declare function toolAvailable(name: string, policy: ToolPolicy): boolean; /** * Which prompt sections this thread can actually act on. * * A section describing tools the thread cannot reach is worse than absent: it is dead * instruction that still costs tokens every turn (BACKGROUND WORK ~90, SCHEDULING ~200, * INTER-AGENT ~80) and invites the model to invent a substitute — the failure mode behind * task 134's fabricated `curl`. */ export interface PromptCapabilities { backgroundWork: boolean; scheduling: boolean; interAgent: boolean; } export declare function promptCapabilities(policy: ToolPolicy): PromptCapabilities; /** * Remove prompt sections whose tools this thread cannot reach. * * Sections in SYSTEM_PROMPT_TEMPLATE are blank-line-delimited blocks whose first line is a * header, so this is a split/filter/join — lossless when nothing is gated (regression-locked), * and a no-op on a custom template that has no such headers. * * Only ever REMOVES text, so an unconstrained thread's prompt is byte-identical and the task * 117 static-prompt budget cannot be breached by this function. */ export declare function applyCapabilityGates(template: string, caps: PromptCapabilities): string; //# sourceMappingURL=tool-inventory.d.ts.map