/** * * Vendor-neutral PreToolUse gate. Reads the standard `{tool, input}` * JSON hook payload (any IDE / harness using this protocol is * supported; the implementation is host-agnostic) and decides * whether the tool call is allowed: * * Edit / Write / MultiEdit on hard-blocked path families * (src/, tests/unit/, tests/integration/, config/, bin/, scripts/) * → deny with `PEAKS_CODE_PROHIBITED_DIRECT_EDIT`. * * All other tool calls, OR allow-listed paths * (.peaks/**, .peaks/_runtime/**, skills/**, docs/**, *.md) * → allow (silent exit 0). * * Pure function. This module is the ONE implementation of the rule: the * shell-script sibling (`pre-tool-code-gate.sh`) is a thin wrapper that * execs the CLI built on it, and its own no-CLI fallback carries a * narrower copy of the same families. */ import { normalizeGatePath, type GateRoots } from './gate-path-normalize.js'; export { normalizeGatePath }; export type { GateRoots }; export type GateToolName = 'Edit' | 'Write' | 'MultiEdit' | string; export type GatePathKey = 'file_path' | 'path' | 'notebook_path'; export interface GateInput { readonly tool: GateToolName; readonly input: Readonly>; } /** Hard-blocked path families — orchestrator MUST NOT Edit/Write these. */ export declare const HARD_BLOCKED_PATH_FAMILIES: readonly ["src/", "tests/unit/", "tests/integration/", "config/", "bin/", "scripts/"]; /** Allow-listed path prefixes / suffixes — orchestrator may freely Edit/Write these. */ export declare const ALLOW_LISTED_PATH_PATTERNS: readonly [".peaks/", ".peaks_", "skills/", "docs/", ".md", "CHANGELOG.md", "README.md"]; /** The tool names this gate judges. A call under any other name is allowed. */ export declare const GATED_TOOL_NAMES: readonly ["Edit", "Write", "MultiEdit"]; /** Is this tool one the gate judges at all? */ export declare function isGatedTool(tool: GateToolName): boolean; export type GateVerdict = { readonly action: 'allow'; } | { readonly action: 'deny'; readonly filePath: string; readonly reason: string; readonly message: string; }; /** Pull the target file path from `input.file_path | input.path | input.notebook_path`. */ export declare function extractFilePath(input: Readonly>): string; /** * Pure decision function. Given the tool name + input object, return * the gate verdict. Tolerates malformed payloads (allow). * * `driver` is the already-resolved identity of whoever is running the * tool call, or `null`/absent when no caller identity resolved. Absent and * `null` are the same verdict as before this parameter existed: the * hard-blocked families deny. Resolution is the caller's job, so this * module keeps no session, lease or filesystem state. * * `roots` is where the verdict is taken: the root the deny families are * relative to, and the directory the tool call runs in. A bare string is the * project root alone, which is how every earlier call spelled it. * * The project root is what lets an ABSOLUTE path be judged like its relative * spelling; without it an absolute path cannot be related to the project and * is left alone. It is the project root the CALLER named, not the canonical * root above it: a project nested inside a git repository is judged against * itself, and the canonical root - where the session and the leases live - is * a different question that this parameter does not answer. * * The tool call's directory is the base a RELATIVE path is read against: the * file a call writes is the one that directory names. It does not move the * families - those stay relative to the project root - it only says which * file the payload points at. * * THE VERDICT IS THE STRICTEST OF THE TWO READINGS. The path is read * LEXICALLY - how it is written - and, when that reading has not already * denied the file, CANONICALLY - where the file really is. A directory can * be named two ways, and the difference is what a symlinked base looks like: * a call that runs in `link/` and writes `deep/x.ts` lands in `src/deep/x.ts` * when `link` points at `src`, and the lexical reading says `link/deep/x.ts` * - no family at all. §18.1 rules that the verdict depends on WHICH * DIRECTORY the file is in, not on how the path is written. * * Strictest, and not "canonical wins": making the canonical reading * authoritative would WIDEN the gate in a layout this repository supports. * When `src` is a symlink to `shared/src`, the lexical reading is * `src/x.ts` (a family) and the canonical one is `shared/src/x.ts` (not * one), and the file the gate denies today must stay denied. The tie-breaker * is the repository's own: a wrongly ALLOWED edit is a hole, a wrongly * REFUSED one is only an inconvenience. So a file reached through a * directory NAMED `src` stays refused even when it really sits in `docs/` - * the spelling is what the path says in this project's terms, and the gate * refuses it rather than resolve it. */ export declare function decideGateAction(tool: GateToolName, input: Readonly>, driver?: string | null, roots?: GateRoots): GateVerdict;