/** * vigiles — Agent runtime: the PreToolUse tool-contract rail. * * A subagent declares an allowed-tools contract in its frontmatter (`tools:`). * But that field is documentation, not a hard runtime boundary (Claude Code * #4740/#21460, SDK #172): permissions are session-wide, a subagent inherits the parent * session's grants, and `tools:` only filters what's *offered* — it can't deny * what the session allows. The deterministic layer that actually closes the gap * is a **PreToolUse hook** that blocks any tool the active agent's contract * doesn't list. * * This is the same emit-a-hook pattern the skill runtime already ships * (`src/adapters/claude-code/skill-runtime.ts`): there a `Stop` hook reads the active skill's * compiled SKILL.md and runs its result gate; here a `PreToolUse` hook reads * the active agent's compiled `.md`, parses its `tools:` allowlist, and * allows/denies the tool call. The compiled markdown's frontmatter is the * single source of truth — the same list that documents intent IS the list the * hook enforces, so the two agree by construction (see `enforcedTools`). * * Which agent is active is recorded in `.vigiles/active-agent.json` — Claude * Code hooks don't surface the dispatched subagent, so vigiles records it * (mirrors `.vigiles/active-skill.json`). The decision logic below is * harness-agnostic and fully testable. */ import type { PurityLevel } from "../../core/effects.js"; import { parseAgentTools, parseAgentToolList } from "./agent-tools.js"; export { parseAgentTools, parseAgentToolList }; /** * Parse the declared purity floor from a compiled agent's `.md` — the * `` marker `compile` emits (see `purityMarker`). * Returns null when no marker is present (the unit declared no floor, so the * purity gate imposes no constraint). The single source of truth the runtime * gate reads, exactly like `tools:` for the tool-contract rail. */ export declare function parseAgentPurity(markdown: string): PurityLevel | null; export interface PreToolDecision { /** Whether the tool call is allowed (true) or blocked (false). */ readonly allow: boolean; /** Message fed back to the model on a block; empty on allow. */ readonly message: string; } /** * Decide whether `tool` is allowed under an agent's tool contract. Pure, so the * rail is unit-testable without spawning anything. * * - `allowed === null` → the agent declared no `tools:` line, so it inherits * everything and the rail imposes no restriction (allow). * - otherwise → allow iff the tool is in the allowlist; deny anything else, * feeding the contract back to the model so it self-corrects. */ export declare function decidePreToolUse(allowed: readonly string[] | null, tool: string): PreToolDecision; /** * Read the active-agent stack (oldest → newest; the dispatched subagent chain). * Back-compat: a legacy single-slot `{ agent: string }` marker reads as a one-frame * stack; a malformed file or non-string entries → an empty stack (fail-open). */ export declare function readActiveStack(cwd: string): string[]; /** * Push a dispatched subagent onto the active stack (a `PreToolUse` spawn). The * PreToolUse gate then enforces this subagent's contract until it returns. Under * nesting each dispatch pushes a frame, so the chain is tracked, not overwritten. */ export declare function pushActiveAgent(cwd: string, agentPath: string): void; /** * Pop the top frame — the subagent returned (`SubagentStop`), so control returns * to its PARENT (the next frame down), whose contract the gate enforces again. * This is the nesting-safe close, distinct from {@link clearActiveAgent} (which * drops the whole stack). Popping an empty stack is a no-op. */ export declare function popActiveAgent(cwd: string): void; /** * Push a subagent frame (the manual `agent-start` fallback + the deterministic * spawn open-signal both use this). A single call is equivalent to a one-frame * stack, so the top — what the gate reads — is this agent. Alias of * {@link pushActiveAgent} kept under the historical name. */ export declare function setActiveAgent(cwd: string, agentPath: string): void; /** * Clear the WHOLE stack (a hard reset / session end). Distinct from * {@link popActiveAgent}, which returns to the parent frame. Idempotent. */ export declare function clearActiveAgent(cwd: string): void; /** * The active agent's compiled `.md` — the STACK TOP — or null when none is active. * The gate reads the top, so a returned nested subagent reveals its parent's * contract again (the contract-escape fix). */ export declare function readActiveAgent(cwd: string): string | null; /** * EXPERIMENTAL — parked (P3), do NOT auto-wire. The subagent-window tracking is * now nesting-safe: a depth-aware STACK (push on dispatch, pop on SubagentStop — * see {@link pushActiveAgent}/{@link popActiveAgent}) closes the contract-escape * the flat single-slot model allowed under Claude Code v2.1.172 depth-5 nesting * (certified in research/prototypes/.../AgentWindowStack.tla). The open signal * recognizes BOTH spawn tool names (`Task` and the nested-spawn `Agent`), gated on * a resolvable `subagent_type` so a non-spawn call never opens a frame. Still * parked because the `effect()` sub-region goal it served was dropped (see * research/effect-boundary-design.md, "Why dropped") — the stack is shipped for * when active-agent contract enforcement under nesting is wanted on its own. * * Resolve a spawn tool's `subagent_type` to the compiled agent `.md` to * activate, or null when none is found. The DETERMINISTIC open signal that * replaces the model-invoked `agent-start`: Claude Code fires `PreToolUse` for * the parent's `Task` dispatch (and `SubagentStop` when it returns), so the * harness — not the model — brackets the subagent's active window. The name is * the last ":"-segment (a `--plugin-dir` subagent_type is namespaced * "plugin:name"); searched in `agents/` under the cwd then the plugin root. A * path under the cwd is returned relative (readActiveAgent resolves vs cwd); a * plugin-root hit is absolute. Returns null on an unknown agent (fail-open: an * unresolved subagent is simply not gated, exactly as before agent-start ran). */ export declare function resolveDispatchedAgent(subagentType: string, cwd: string, pluginRoot?: string): string | null; /** * The agent `.md` to activate for a `PreToolUse(Task)` event, or null. Pure * (reads `tool_input.subagent_type`, resolves via {@link resolveDispatchedAgent}). */ export declare function decideTaskDispatch(toolInput: unknown, cwd: string, pluginRoot?: string): string | null; /** * PreToolUse-hook decision. If an agent is active, enforce BOTH deterministic * rails its compiled `.md` declares, in order: * * 1. the tool-contract rail (`tools:`) — allow only listed tools; * 2. the purity gate (`vigiles:purity:`) — allow only calls within the declared * effect floor, refining `Bash` by the live `command` (`decidePurityGate`). * * The first to deny wins, feeding its reason back to the model. With no active * agent (or one that declared neither contract), always allow — the rails only * constrain agents that opted in. */ export declare function evaluatePreToolUse(cwd: string, tool: string, command?: string): PreToolDecision; //# sourceMappingURL=agent-runtime.d.ts.map