/** * @packageDocumentation * EXT-58 — **rung-aware tool descriptions** (`docs/gaunt-sloth-2.0/approvals-and-rater-spec.md` * §4.5). * * §4.5 is the *prevention* half of the approvals design. Shell commands are gated; built-in tools * at or below the current rung are not, so a model that reaches for the shell to do something a * granted built-in already does converts a free action into an interruption. A model chooses a * tool by reading that tool's description, so the approvals posture is stated **there** — not as a * global preference in the system prompt, which sits far from the decision and can drift out of * step with the gate. * * Two properties are load-bearing and are why this module exists at all: * * 1. **A tool that is auto-approved gets NO suffix.** The absence of the sentence is what marks it * free. Appending "this tool is already approved" would be the opposite of the design. * 2. **The suffix is derived from the rung in force AND from the set of tools that rung actually * gates.** "A description that disagrees with what the gate will actually do is worse than no * description at all" (§4.5), so {@link isGrantedAtRung} takes the LIVE gated set as a parameter * and the agent passes the SAME `resolveGatedToolNames` result. The two can therefore not * drift: widening what a rung gates ([[EXT-30]]) widens the descriptions in the same breath. * That set is narrower than the rung-independent one wired into the interrupt * (`resolveInterruptToolNames`), because a call the live rung does not gate is approved on * arrival and so is genuinely free. * * **Scope, per §4.3.** The *rater* covers the shell only: every other gated tool goes to the human * without a rating call until [[EXT-30]] widens the rater. The *gate* is wider than the rater. At * the two deterministic rungs (`manual`, `write`) the agent gates every bound tool the rung's * access class does not auto-grant — the write built-ins, the shell, MCP tools (whatever their * `readOnlyHint` says) and custom tools — so at those rungs a non-granted call always reaches the * human. At `assisted`, `auto` and `bypass` the gated set is the shell alone. * * `config/shell-policy.ts`'s `isToolGatedAtRung` is the single rule for all of that, and both the * live gated set and this module's grant are projections of it, so what a rung gates and the grant * reported here cannot disagree. */ import type { ApprovalRung } from '#src/config/shell-policy.js'; /** * §4.5's table, **verbatim**. The wordings are normative copy the model reads; do not paraphrase, * re-punctuate or "improve" them. * * - `manual` and `write` share one sentence: at both rungs a non-granted call goes to the human, * so the user's approval is a certainty, not a possibility. * - `assisted` softens `will` to `MAY`: the rater approves what it rates safe, so only some calls * reach the user. * - `auto` has its own sentence, and the difference it states is real: [[EXT-29]]'s negotiation * hands a call the rater will not clear back to the **model**, with the reason, so the first * consequence of an unsafe-looking call there is a refusal it can answer rather than a person * being asked — what changes is who answers first, and that is the one thing a model choosing a * tool can act on. A shared wording would hide it in the model's own tool-selection input. * * Its `MAY` therefore sits **outside** the contrast the three bullets above draw: it qualifies * *"be refused by the auto-rater"*, not *"require the user's approval"*, so this sentence says * nothing either way about whether a person can be asked. That silence is deliberate — the * string is §4.5's row verbatim — and its cost is recorded here rather than left to be * discovered: `catastrophic` escalates to a human on the FIRST hit with no rounds at all, so a * person *can* be reached at `auto` and this input does not say so. Silent is not false, which * is why the wording stands and why the premise quoted above still holds; a wording claiming the * user is never asked would be false. * - `bypass` appends nothing to anything: no gate, so no sentence could be true. */ export declare const RUNG_TOOL_DESCRIPTION_SUFFIXES: Readonly>; /** * The access class of a built-in tool, in the sense §2 grants them: `read` = reading and listing * files within the working folder; `write` = creating, editing, moving and deleting files within * the working folder. A tool that does neither (the shell, a network fetch, an MCP call) has no * class and is granted by no rung but `bypass`. */ export type BuiltInToolAccess = 'read' | 'write'; /** * Access class per built-in tool name. Keyed by NAME, not by owner, so it covers gsloth's own * `GthFileSystemToolkit` and the conventional filesystem names (`ls`/`glob`/`grep`, and the * `read_file`/`write_file`/`edit_file` the two conventions share) that a graph builder or an MCP * server may register instead. One flat table serves them all. * * Deliberately absent: `run_shell_command`, the fixed dev-command tools, * `gth_web_fetch`, `gth_checklist`, `gth_status_update`, `show_a2ui_surface`, MCP/custom/A2A tools. * None of them is "reading or writing files in the working folder", so none is granted by a rung's * access class, and at `manual`/`write` every one of them escalates to the human. * * **An MCP tool's own `readOnlyHint` earns it nothing here.** A server's self-declared annotation is * least earned exactly where the ladder is strictest, so it must not skip the prompt at those two * rungs; membership of this table is the only thing that does. Adding a name here grants it at a * rung, so add one only for a tool that genuinely reads or writes files in the working folder. */ export declare const BUILT_IN_TOOL_ACCESS: Readonly>; /** The sentence §4.5 appends at this rung, or `null` when the rung appends nothing (`bypass`). */ export declare function getRungToolDescriptionSuffix(rung: ApprovalRung): string | null; /** * §2 — does `rung`'s own grant cover this tool's **access class**, i.e. is the tool free *on its * class alone*, before anything is asked about whether the gate gates it? * * - `read` — granted at every rung (§2.1). * - `write` — granted from `write` up (§2.2, and §2.3/§2.4 which grant "everything `write` grants"), * so it escalates at `manual`. * - **no class at all** — the shell, a network call, an MCP tool, a custom or * agent-authored tool: granted by no rung. There is no implicit exemption; a tool is free here * only by appearing in {@link BUILT_IN_TOOL_ACCESS}. * * **This is the one implementation of that rule.** {@link isGrantedAtRung} decides a grant with it, * and `config/shell-policy.ts`'s `isToolGatedAtRung` decides gated-set membership with it — which * is in turn what the agent's interrupt set and `GthAgentRunner`'s own live-rung check are built * from. So the gate cannot escalate a call the descriptions and the rater's granted list call free * — the drift §4.5 names as worse than having no description at all. Two derivations of one rule is * exactly what this function exists to prevent; do not inline it back into either caller. * * `bypass` is not special-cased: it grants everything for a reason unrelated to access class (the * gate is off), which {@link isGrantedAtRung} states where that belongs. */ export declare function isAccessClassGrantedAtRung(toolName: string, rung: ApprovalRung): boolean; /** * Is `toolName` auto-approved (granted, free, no prompt and no rating) at `rung`? * * @param toolName The registered tool name. * @param rung The rung in force for the session. * @param gatedTools The LIVE gated set — what `rung` actually gates, i.e. * `resolveGatedToolNames` for the rung in force. **This is the parameter that keeps the * descriptions honest**: a tool the rung does not gate cannot require approval, whatever a rung's * table row says about tool classes, so it is granted. It is deliberately NOT the wider, * rung-independent set the agent wires into `interruptOn` (`resolveInterruptToolNames`): a call * the live rung does not gate is auto-approved the moment it arrives at the runner, so describing * it as needing approval would be a promise nothing keeps. * * Order: * 1. `bypass` grants everything (§2.5) — the gate is off. * 2. A tool the live rung does not gate cannot require approval, so it is granted at every rung. * 3. A gated tool is granted only where the rung's own grant covers its access class — * {@link isAccessClassGrantedAtRung}, the same rule `resolveGatedToolNames` selects the gated set * with. At the two deterministic rungs those two uses are complementary by construction: a tool * is gated there precisely when this returns false, so step 3 answers false for every tool step 2 * let through, and the gate and the description say the same thing. */ export declare function isGrantedAtRung(toolName: string, rung: ApprovalRung, gatedTools: readonly string[]): boolean; /** * Remove a previously-appended §4.5 suffix (any rung's), returning the tool's own description. * * Makes {@link applyRungAwareToolDescriptions} idempotent and re-appliable at a different rung — * which matters because a resolver may hand back the SAME tool instance on a re-init (an MCP * client caches its tool objects), and a second pass would otherwise stack sentences. */ export declare function stripRungToolDescriptionSuffix(description: string): string; /** The minimal shape {@link applyRungAwareToolDescriptions} needs; keeps this module langchain-free. */ export interface DescribableTool { name?: string; description?: string; } /** * §4.5 — at tool-registration time, append the rung's sentence to the description of every tool * that is NOT auto-approved at that rung, and leave every granted tool's description untouched. * * Mutates in place (and returns the same array): the tools are about to be handed to * `createAgent`, and the array is freshly resolved per init. Idempotent — any * previously-appended suffix is stripped first, so re-registration at a different rung replaces * the sentence rather than stacking one. * * A tool with no name is left alone (provider-native "magic object" tools carry no name and cannot * be classified); a tool with an empty description gets the suffix as its whole description rather * than a leading space. */ export declare function applyRungAwareToolDescriptions(tools: T[], options: { rung: ApprovalRung; gatedTools: readonly string[]; }): T[];