/** A tool call presented to a policy before it executes. */ export interface ToolCallRequest { toolName: string; args: unknown; toolCallId: string; /** * design/80 D-E-core: read-only per-run budget snapshot so a STATELESS policy can self-limit against * state that survives suspend/resume (a fresh in-memory counter would reset every leg — the supervisor * escalation-budget gap). Built by core at prepare time from the DURABLE resource ledger * ({@link import("./checkpoint-store.js").ResourceLedger}) + the durable suspend chain; the policy CANNOT * widen it (no write-back). Present on every gated `check()` once the runner builds the gate; absent only * when a policy is invoked directly (outside a Runner). A NORMAL-ask escalation policy reads it; a SAFETY * ask (egress / irreversible) ignores it and ALWAYS asks (invariant #2 stays structural — core mints * `irreversible_ask` for safety tools regardless of any budget). */ budget?: { /** * Durable resource $ this run may STILL spend (micro-USD); `undefined` = no $ ceiling configured. Frozen * total, prior-wins → it monotonically SHRINKS across legs and is NEVER re-granted by a resume (a * deny-resume continues with the same frozen remaining). **NEVER falsy-test** (`0` = exhausted, NOT * "unlimited" — only `undefined` is unlimited). */ resourceRemainingMicroUsd?: number; /** Durable cumulative resource $ already spent across all prior legs (micro-USD); accumulates, never refunded. */ resourceSpentMicroUsd: number; /** How many times this run has already suspended (the durable suspend chain). Lets the policy tighten as * the chain grows — the precise complement to the unconditional `suspendLoopCap` backstop. */ suspendCount: number; }; } /** Where a decision came from, for audit (design/37). */ export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier"; /** * A three-state permission decision for a tool call (design/37). Upgrades the old two-state * `{allow|deny}`: * - `allow` may carry `updatedInput` — REWRITTEN args (redact a secret, clamp a path/value) that * REPLACE the model's args before execution (re-validated against the tool schema first). * `deny` may ALSO carry `updatedInput` (1.256 复审 MED-2): it never executes anything, but a * combined-policy deny reports the ACCUMULATED pre-deny rewrite (policy A redacted → policy B * denied) so a deny OBSERVER (`Hooks.permissionDenied`) sees the final post-rewrite args, not the * stale un-redacted ones. * - `ask` requests human confirmation; with no approver wired it resolves deterministically to deny * (headless auto-deny, see `RunnerDeps.onAsk`). * - `deny` blocks the call. * Human-readable text is `message` (preferred) or the legacy `reason`; read it via {@link decisionText}. * **Backward-compatible:** the old `{action:"allow"}` / `{action:"deny", reason}` are valid subsets. */ export type PermissionResult = { action: "allow"; updatedInput?: unknown; message?: string; reason?: string; decisionReason?: DecisionReason; } | { action: "ask"; updatedInput?: unknown; message?: string; reason?: string; decisionReason?: DecisionReason; } | { action: "deny"; updatedInput?: unknown; message?: string; reason?: string; decisionReason?: DecisionReason; }; /** @deprecated alias kept for back-compat — `PermissionResult` is the canonical three-state type (design/37). */ export type ToolDecision = PermissionResult; /** The human/model-readable text of a decision: `message` wins, falling back to the legacy `reason`. */ export declare function decisionText(d: PermissionResult): string | undefined; /** * Gates tool calls before they execute (the production safety layer for "processing/changing data"). * * `check` runs before each tool call. Return `{ action: "deny", reason }` to block it — the model * receives an error tool result with the reason and can continue or report blocked. Return * `{ action: "allow" }` to permit it. * * `check` may be async, which is also how **human-in-the-loop approval** works: a deployment can * hold the promise open until an operator approves/denies. `signal` fires when the task aborts * (timeout / max turns / cancel) — honor it to release a pending approval instead of hanging (F4). * The Runner also races `check` against `signal` itself, so a policy that ignores it still cannot * hang the worker past the deadline; passing it through just lets you clean up the wait early. */ export interface ToolPolicy { check(req: ToolCallRequest, signal?: AbortSignal): ToolDecision | Promise; } /** * Name-based allow/deny. `deny` always wins. If `allow` is provided, only those tools are permitted * (everything else denied). Without `allow`, everything not in `deny` is permitted. */ export declare function createAllowDenyPolicy(opts: { allow?: string[]; deny?: string[]; }): ToolPolicy; /** * Human-in-the-loop approval for selected tools. Tools in `requireApproval` call `approve(req)` and * are allowed only if it resolves true; tools in `deny` are always blocked; everything else is allowed * (override with `denyByDefault: true` to allow only `requireApproval` + an explicit `autoAllow`). */ export declare function createApprovalPolicy(opts: { /** Tools that need an approval decision. */ requireApproval: string[]; /** * The approval decision (e.g. await an operator). Resolve true to allow, false to deny. `signal` * fires when the task aborts — race your wait against it (e.g. an OA approval callback) so a * never-answered request is released at the deadline rather than holding the worker. */ approve: (req: ToolCallRequest, signal?: AbortSignal) => boolean | Promise; /** Always-denied tools. */ deny?: string[]; /** Always-allowed tools (only meaningful with `denyByDefault`). */ autoAllow?: string[]; /** When true, deny anything not in `requireApproval` or `autoAllow`. Default false. */ denyByDefault?: boolean; /** * Optional self-contained deadline (ms): if `approve` hasn't resolved by then, deny. Belt-and- * suspenders for **direct** callers — the Runner already races `check` against the task signal, so * a Runner-driven task is bounded regardless; this protects a caller invoking `check()` without a * signal. Omit to wait indefinitely on `approve`. */ approvalTimeoutMs?: number; }): ToolPolicy; /** * Combine policies with the three-state fold **deny > ask > allow** (design/37): the WORST outcome * across all policies wins. A `deny` short-circuits (nothing can override it); an `ask` does NOT * short-circuit — scanning continues, because a later policy may still `deny` (which outranks ask). * With no deny, the first `ask` wins; with neither, `allow`. * * **Idempotency requirement:** because the fold continues past an `ask` to look for a later `deny`, a * policy's `check()` can be invoked even after an earlier policy returned `ask`. `check()` MUST be * idempotent — repeated calls with the same {@link ToolCallRequest} return the same decision and * accumulate no side effects (e.g. don't fire a duplicate approval notification on each call). */ export declare function combinePolicies(...policies: ToolPolicy[]): ToolPolicy; /** * design/center §10 — a COARSE allow/deny gate keyed on the LEADING command NAME (`argv[0]`) of a single * simple shell command. It reuses the ONE shared simple-command parser * ({@link import("../tools/fs/index.js").parseLeadingCommandName}) — it does NOT re-implement argv[0] parsing * (a second parser would drift and open a bypass). For a `bash`/`bash_readonly` call it extracts the leading * command name and: * - if the name is in `deny` → `deny` (deny-wins, matching {@link createAllowDenyPolicy}; NOT last-match-wins, * which would fight {@link combinePolicies}'s deny>ask>allow fold); * - else if `allow` is provided and the name is NOT in it → `defaultAction` (`"ask"` default, or `"deny"`); * - else → `allow`. * A command the parser cannot reduce to a single bare name (it has shell operators / a path-prefix / a leading * env-assignment) is treated by `defaultAction` (fail-toward-gate): such a command is exactly what would bypass * an argv[0] filter, so it should not silently `allow`. NON-shell tools are out of scope → `allow` (this gate * only speaks about shell command names; compose it with other policies for the rest). * * **core ships NO opinionated command list** — only this mechanism. The actual `allow`/`deny` rule set is * injected by the profile/config/deployment. * * ⚠️ **NOT A SANDBOX — coarse filter / defense-in-depth only.** This matches `argv[0]` NAME and nothing else, * so the bypass surface is large and the gate MUST NOT be relied on as containment: * - command substitution / subshells (`$(curl …)`, backticks), pipes, redirects, `;`/`&&` chaining — all * rejected as un-parseable here (→ `defaultAction`), so they don't sneak past `allow`, but that is a GATE * not a guarantee of safety; * - an indirection wrapper runs an arbitrary inner program under an allowlisted `argv[0]`: `sh -c '…'`, * `env FOO=bar curl …`, `sudo rm …`, `timeout 5 curl …`, `xargs curl`, `find . -exec rm {} \;`, * `python -c '…'`, `npm run