import type { IntegrationErrorCode, Judge, Questions } from "pi-typesafe"; import type { ActionGuardConfig, ArmingRule, CommandRule, PathRule, RulesConfig, SecurityConfig, SlopGuardConfig } from "./config.js"; export type Level = "allow" | "warn" | "confirm" | "deny"; export type Severity = "destructive" | "risky" | "sensitive" | "deny"; export type ViolationSource = "pattern" | "rules-guard" | "security-guard" | "slop-guard"; /** Scope of a violation for deterministic authorization matching. */ export interface ViolationScope { /** File paths involved, e.g., ["eval/reports/"] */ paths?: string[] | undefined; /** The full shell command, if bash */ command?: string | undefined; /** The tool name, e.g., "bash", "write", "edit" */ tool?: string | undefined; /** For per-target violations (e.g., each rm target): which target this violation represents. */ targetIndex?: number | undefined; /** Total number of targets in the original command (for informational purposes). */ targetCount?: number | undefined; } /** A pattern detection result enriched with severity, authorization eligibility, and scope. */ export interface Violation { id: string; severity: Severity; source: ViolationSource; description: string; /** The pi-warden.md rule text, if source is "rules-guard" */ matchedRule?: string; /** Groups related patterns (e.g., "rm" covers rm, git-rm, find-delete) */ patternFamily?: string; /** For authorization: paths, files, or targets affected */ scope?: ViolationScope; } /** Result of deterministic authorization analysis for one violation. */ export interface Authorization { /** true only if action + scope match AND no negation */ authorized: boolean; /** prompt contains the action verb */ actionMatched: boolean; /** prompt references the affected paths/targets (when scope exists) */ scopeMatched: boolean; /** prompt negates the action ("don't", "do not", "never", "skip") */ negated: boolean; } /** A violation after escalation rules have been applied. */ export interface EscalatedViolation extends Violation { escalatedSeverity: Severity; } export interface PatternHit { id: string; severity: Severity; /** Short human label; never contains the matched text. */ label: string; /** For user-defined confirm rules: dialog prompts the user, hold uses steer semantics. */ action?: "dialog" | "hold"; /** Optional user-defined message, shown instead of the derived label. */ message?: string; } export interface ActionInput { tool: string; input: Record; cwd: string; /** Latest user request, used to judge whether the action is on task. */ task?: string | undefined; /** Prior conversation clarifies scope, but never grants approval for a held action. */ context?: readonly TaskMessage[] | undefined; /** The agent's own words in the message that makes this call (or its latest text under this prompt). Explains the step; never authorizes it. */ plan?: string | undefined; } export interface TaskMessage { role: "user" | "assistant"; text: string; } /** Redacted, truncated view of a tool call. This object is what leaves the machine. */ export interface ActionSummary { tool: string; command?: string; path?: string; location?: "inside_project" | "outside_project"; exists?: boolean; bytes?: number; excerpt?: string; editCount?: number; edits?: Array<{ oldText: string; newText: string; }>; input?: string; /** Present when part of the command is data (a heredoc body, a quoted message), so a destructive string inside it is payload. */ dataText?: string; } export type ScopeLabel = "expected_step" | "plausible_side_step" | "unrelated" | "unclear"; export interface Judgment { irreversible: number; offTask: number; scope: ScopeLabel; scopeConfidence: number; /** P(the latest user message approves this exact action); only asked when a previously held call is retried. */ approved?: number; /** P(the latest user message regrets an allowed call of the previous turn); asked once per prompt, on its first action request. */ regretted?: number; /** The id of the regretted previous action when several were offered. */ regretTarget?: string; securityRisk?: number; /** P(the action changes files, state, or external systems). Off-task alone holds only actions that can change something. */ mutates?: number; /** P(the action does something materially different from `plan`); only asked when the agent said something before the call. */ intentMismatch?: number; /** P(the effect is visible outside the working tree: commit, push, merge, publish, message, install, launched process); commands only. */ visible?: number; /** P(action is safe to proceed without asking). Inverted: low = hold. */ shouldProceed?: number; model: string; elapsedMs: number; } /** One probability per slop symptom; the steer names the symptoms above the threshold. */ export interface SlopJudgment { stub: number; comments: number; dead: number; hedging: number; } export type SlopSymptom = keyof SlopJudgment; export declare const SLOP_SYMPTOMS: readonly SlopSymptom[]; /** A call the guard allowed in the previous turn, as the regret question sees it: redacted summary fields only. */ export interface PreviousAction { id: string; tool: string; command?: string; path?: string; } export interface Verdict { level: Level; source: "skipped" | "read-only" | "pattern" | "typesafe" | "error"; summary: ActionSummary; patterns: PatternHit[]; /** Human-readable reasons without secrets or full commands. */ reasons: string[]; judgment?: Judgment; slop?: SlopJudgment; /** Symptoms at or above the slop threshold, strongest first. The level itself is never raised by slop. */ slopSymptoms?: SlopSymptom[]; slopReasons?: string[]; /** True when a previously held call was allowed because the user's latest message approves it. */ approvedByUser?: boolean; /** Redacted, truncated `plan` as sent to Jev and shown in the trace. */ plan?: string; /** True when Jev finds the call at odds with the agent's stated plan and the call can change something; the agent is told. */ intentMismatch?: boolean; /** True when Jev finds the call unrelated to the request on a call that can change something. Still steered in the reason log, but the steer message is suppressed until AUC improves above 0.51. */ offTaskSteer?: boolean; /** True when should_proceed is below the hold threshold; the agent is told to pause and ask. */ shouldProceedSteer?: boolean; shouldProceedTraceOnly?: boolean; shouldProceedTraceOnlyReasonIndex?: number; /** Off-task steer is recorded in the trace but not delivered to the agent; the score has no reliable signal yet (AUC 0.51). */ offTaskTraceOnly?: boolean; /** Index of the trace-only off-task diagnostic; later reasons append, and any prepend must adjust this index. */ offTaskTraceOnlyReasonIndex?: number; /** Answers to the caller's own `questions`: P(yes) for a noul, the picked option for a choice, the level for a score. */ extra?: Record; /** Safe TypeSafe error message when the judge could not answer. */ error?: string; errorCode?: IntegrationErrorCode; } export type { Judge } from "pi-typesafe"; export interface EvaluateOptions { config: ActionGuardConfig; /** Omit to run offline pattern checks only (no consent, no network). */ judge?: Judge | undefined; signal?: AbortSignal | undefined; /** Adds quality questions for write/edit content to the same request. */ slop?: SlopGuardConfig | undefined; security?: SecurityConfig | undefined; /** * The rules guard's switch. The active rules file rides every judged action request, so `enabled: false` is what keeps * that content on this machine. Omitted, the file is sent as before, so a library caller that passes no rules config * keeps today's behaviour. */ rules?: Pick | undefined; /** This exact call was held earlier and the user has replied since: ask whether the reply approves it. */ retryAfterHold?: boolean | undefined; /** Calls allowed in the previous turn: ask whether the user's latest message regrets one of them (rides this request). */ previousActions?: readonly PreviousAction[] | undefined; /** * Extra questions over the same state (`task`, `context`, `plan`, `action`), answered in `verdict.extra` and never acted on. * How a candidate question is measured on recorded sessions before it earns an acting rule (scripts/calibrate-action.mjs). */ questions?: Questions | undefined; } export declare const higher: (a: Level, b: Level) => Level; interface Rule { id: string; severity: Severity; label: string; test: RegExp; } export declare const SHELL_RULES: Rule[]; export interface ScannedCommand { /** The command with data text blanked; what the pattern rules read. */ text: string; /** True when a heredoc body or quoted data was removed. */ stripped: boolean; } /** * Removes heredoc bodies that are not fed to a shell and quoted arguments of data commands. Interpreter heredocs * (`python3 - <; /** Exempt ids that name neither a built-in, a classifier id, nor one of the user's own rules: inert, but * almost certainly not what the user meant. */ export declare function unknownExemptIds(exemptRules: readonly string[], commandRules?: readonly CommandRule[], commandDenyRules?: readonly CommandRule[], pathRules?: readonly PathRule[], armingRules?: readonly ArmingRule[]): string[]; /** Path rules whose access + tools combo means they can never fire: `access:"write"` (reads held) with * only write tools (writes flow, nothing to hold), or `tools:["read"]` when `read` is not in `action.tools` * (the read tool is never inspected). Reported once at load, like unknown exempt ids. */ export declare function inertPathRules(pathRules: readonly PathRule[], actionTools: readonly string[]): string[]; export declare function matchPatterns(tool: string, input: Record, cwd?: string, options?: PatternOptions): PatternHit[]; export declare function writeSinkTargets(command: string): string[]; /** Shared glob/regex path matcher used by both path rules (guard.ts) and arming rules (arming.ts). * Normalises ~ expansion and path separators, then tries the pattern in both tilde-prefixed and bare forms. */ export declare function matchPathGlobs(patterns: readonly string[], useRegex: boolean, candidate: string): boolean; export declare function matchPathRules(tool: string, input: Record, cwd: string | undefined, rules: readonly PathRule[] | undefined, exempt: Set): PatternHit[]; export declare function isReadOnlyCommand(command: string): boolean; export declare function describeAction(tool: string, input: Record, cwd: string): ActionSummary; /** Parsed answer for one violation_judgments choice question. */ export interface ViolationJudgmentAnswer { violated: boolean; confidence: number; } /** Parse violation_judgment answers from verdict.extra. Keys use per-instance index (`violation_`) so two * violations with the same pattern ID but different scopes receive independent answers. */ export declare function parseViolationJudgments(violations: readonly Violation[], extra: Record | undefined): ViolationJudgmentAnswer[]; export declare const questions: { irreversible: import("pi-typesafe").NoulQuestion; off_task: import("pi-typesafe").NoulQuestion; mutates: import("pi-typesafe").NoulQuestion; scope: import("pi-typesafe").ChoiceQuestion<{ readonly expected_step: "Required implementation, bug fix, regression test, or verification for the active task"; readonly plausible_side_step: "Reasonable supporting work whose necessity is not yet established"; readonly unrelated: "No useful connection to the active task, or contrary to the user's current direction"; readonly unclear: "The supplied conversation or action gives too little information to establish scope; this is not itself a violation"; }>; }; /** * Commands only (a write or edit never is). Alone it has no precision, but a visible action that departs from the agent's * plan is what users object to: on recorded sessions 18% of such calls sat in a turn the user rejected, four times the base rate. */ export declare const visibleQuestion: { visible: import("pi-typesafe").NoulQuestion; }; /** Asked only when the agent said something before the call; an empty plan cannot be contradicted. */ export declare const intentQuestion: { intent_mismatch: import("pi-typesafe").NoulQuestion; }; /** Unified gate: rule violations, unrequested scope, explicit constraint breaches, and material user decisions. Inverted: low score = hold. Calibrated: AUC 0.07 (inverted) against regret, 0.64 against rejected turns (100 targeted sessions, 2026-09-20). */ export declare const shouldProceedQuestion: { should_proceed: import("pi-typesafe").NoulQuestion; }; export declare const slopQuestions: { slop_stub: import("pi-typesafe").NoulQuestion; slop_comments: import("pi-typesafe").NoulQuestion; slop_dead: import("pi-typesafe").NoulQuestion; slop_hedging: import("pi-typesafe").NoulQuestion; }; export declare const SLOP_LABELS: Record; export declare const securityQuestion: { security_risk: import("pi-typesafe").NoulQuestion; }; export declare const approvalQuestion: { approved: import("pi-typesafe").NoulQuestion; }; /** * One yes/no on whether the user's reply regrets what the agent did last turn; with several candidates a Choice names the * one. Labels the allowed calls for hold calibration and never changes the verdict on the current call. */ export declare function regretQuestions(actions: readonly PreviousAction[]): { regretted: import("pi-typesafe").NoulQuestion; regret_target?: never; } | { regretted: import("pi-typesafe").NoulQuestion; regret_target: import("pi-typesafe").ChoiceQuestion<{ [k: string]: string; }>; }; /** Scores, counts, and whitespace removed; the fingerprint of what the notice actually says. */ export declare function steerFingerprint(content: string): string; export declare class SteerRepeatWindow { private readonly window; private readonly recent; constructor(window?: number); /** True when this normalised text was already sent inside the window; the text is recorded either way. */ seen(content: string): boolean; reset(): void; } /** The agent's words as they leave the machine: redacted and bounded. Undefined when the agent said nothing. */ export declare function describePlan(plan: string | undefined): string | undefined; export declare function buildRequest(summary: ActionSummary, task: string | undefined, extras?: { slop?: boolean; approval?: boolean; security?: boolean; context?: readonly TaskMessage[] | undefined; previousActions?: readonly PreviousAction[] | undefined; plan?: string | undefined; questions?: Questions | undefined; rules?: string | undefined; rulesSource?: string | undefined; violations?: readonly Violation[] | undefined; floorHits?: string; }): { state: { floor_hits?: string; rulesSource?: string; rules?: string; previous_actions?: { command?: string; id: string; tool: string; path?: string; }[]; plan?: string; task: string; action: Record; context: { role: "user" | "assistant"; text: string; }[]; }; questions: { regretted: import("pi-typesafe").NoulQuestion; regret_target?: never; security_risk?: import("pi-typesafe").NoulQuestion; approved?: import("pi-typesafe").NoulQuestion; slop_stub?: import("pi-typesafe").NoulQuestion; slop_comments?: import("pi-typesafe").NoulQuestion; slop_dead?: import("pi-typesafe").NoulQuestion; slop_hedging?: import("pi-typesafe").NoulQuestion; intent_mismatch?: import("pi-typesafe").NoulQuestion; visible?: import("pi-typesafe").NoulQuestion; irreversible: import("pi-typesafe").NoulQuestion; off_task: import("pi-typesafe").NoulQuestion; mutates: import("pi-typesafe").NoulQuestion; scope: import("pi-typesafe").ChoiceQuestion<{ readonly expected_step: "Required implementation, bug fix, regression test, or verification for the active task"; readonly plausible_side_step: "Reasonable supporting work whose necessity is not yet established"; readonly unrelated: "No useful connection to the active task, or contrary to the user's current direction"; readonly unclear: "The supplied conversation or action gives too little information to establish scope; this is not itself a violation"; }>; should_proceed: import("pi-typesafe").NoulQuestion; } | { regretted: import("pi-typesafe").NoulQuestion; regret_target: import("pi-typesafe").ChoiceQuestion<{ [k: string]: string; }>; security_risk?: import("pi-typesafe").NoulQuestion; approved?: import("pi-typesafe").NoulQuestion; slop_stub?: import("pi-typesafe").NoulQuestion; slop_comments?: import("pi-typesafe").NoulQuestion; slop_dead?: import("pi-typesafe").NoulQuestion; slop_hedging?: import("pi-typesafe").NoulQuestion; intent_mismatch?: import("pi-typesafe").NoulQuestion; visible?: import("pi-typesafe").NoulQuestion; irreversible: import("pi-typesafe").NoulQuestion; off_task: import("pi-typesafe").NoulQuestion; mutates: import("pi-typesafe").NoulQuestion; scope: import("pi-typesafe").ChoiceQuestion<{ readonly expected_step: "Required implementation, bug fix, regression test, or verification for the active task"; readonly plausible_side_step: "Reasonable supporting work whose necessity is not yet established"; readonly unrelated: "No useful connection to the active task, or contrary to the user's current direction"; readonly unclear: "The supplied conversation or action gives too little information to establish scope; this is not itself a violation"; }>; should_proceed: import("pi-typesafe").NoulQuestion; } | { security_risk?: import("pi-typesafe").NoulQuestion; approved?: import("pi-typesafe").NoulQuestion; slop_stub?: import("pi-typesafe").NoulQuestion; slop_comments?: import("pi-typesafe").NoulQuestion; slop_dead?: import("pi-typesafe").NoulQuestion; slop_hedging?: import("pi-typesafe").NoulQuestion; intent_mismatch?: import("pi-typesafe").NoulQuestion; visible?: import("pi-typesafe").NoulQuestion; irreversible: import("pi-typesafe").NoulQuestion; off_task: import("pi-typesafe").NoulQuestion; mutates: import("pi-typesafe").NoulQuestion; scope: import("pi-typesafe").ChoiceQuestion<{ readonly expected_step: "Required implementation, bug fix, regression test, or verification for the active task"; readonly plausible_side_step: "Reasonable supporting work whose necessity is not yet established"; readonly unrelated: "No useful connection to the active task, or contrary to the user's current direction"; readonly unclear: "The supplied conversation or action gives too little information to establish scope; this is not itself a violation"; }>; should_proceed: import("pi-typesafe").NoulQuestion; }; }; export declare function evaluateAction(action: ActionInput, options: EvaluateOptions): Promise; /** What the agent reads after a call that differs from its own plan ran: name the gap, bound the answer to one line. * Without the bound the model writes a full accounting of the notice at the end of every task, which is noise for the * user reading the transcript; the wording below caps the demanded reply at one short sentence. */ export declare function intentSteer(verdict: Verdict): string; /** What the agent reads when should_proceed is low: pause and ask the user. * One line; the agent must not have forwarded the call without consulting the user. */ export declare function shouldProceedMessage(verdict: Verdict): string; /** What the agent reads after an unrelated change ran: the request it drifted from, the two acceptable moves, one line. */ export declare function offTaskSteer(verdict: Verdict): string; /** Offline stand-in for the approval question when TypeSafe is not available. */ export declare function textApproves(task: string | undefined): boolean; /** * The text the agent receives when a call is held. It explains the judgment and the two acceptable next moves, * so the model re-plans instead of retrying. Contains no command text (the model already has it) and no secrets. */ export declare function steerReason(verdict: Verdict, options: { canApprove: boolean; }): string; /** Check if a negator precedes the action verb within 40 characters. */ export declare function isNegated(prompt: string, actionVerb: string): boolean; /** Deterministic scope matching: exact path or basename. For command-scoped violations * (bash with no file paths), scope is not required to match — the verb family alone * determines authorization. File-scoped violations require the prompt to mention the path. */ export declare function scopeMatches(prompt: string, scope: ViolationScope): boolean; /** Full authorization check for one violation against the user's prompt. * Requires: (1) action verb present in the prompt, (2) no negation, (3) scope match. * Scope matching for command-scoped violations requires the full command text. * Scope matching for path-scoped violations requires every path to appear exactly. */ export declare function authorize(prompt: string, violation: Violation): Authorization; /** Derive ViolationScope from tool input. */ export declare function scopeFromInput(tool: string, input: Record): ViolationScope | undefined; /** Whether a violation with this severity is authorization-eligible. Hard denies and sensitive-path violations are not. */ export declare function isAuthEligible(severity: Severity): boolean; /** Convert PatternHit[] to Violation[] with scope. * For rm-family hits, produces one violation per target so authorization and scope checks * are per-target (the prompt must name each target the user wants to authorize). * For non-rm hits, the full command is included in the scope for exact matching. */ export declare function patternHitsToViolations(hits: readonly PatternHit[], tool: string, input: Record): Violation[]; /** Escalation A: blast-radius / action authorization. */ export declare function escalateBlastRadius(violation: Violation, authorization: Authorization, jevJudgment: { violated: boolean; confidence: number; }, config: { escalationThreshold: number; }): Severity; /** Escalation B: rules guard / content violations. */ export declare function escalateRulesViolation(violation: Violation, jevJudgment: { violated: boolean; confidence: number; }, config: { escalationThreshold: number; }): Severity; /** Aggregate: final level is the highest severity among all remaining violations. */ export declare function aggregateLevel(violations: readonly EscalatedViolation[]): Level; /** Remove authorized violations from the set. Returns only non-authorized violations. */ export declare function removeAuthorized(violations: readonly Violation[], authorizations: readonly Authorization[]): Violation[]; /** One-line rendering for widgets and logs. Includes no command text. Templates: see widget.ts. */ export declare function formatVerdict(verdict: Verdict, template?: string): string; /** Render a verdict and return both the line and raw tokens, for live-mode re-rendering. */ export declare function formatVerdictTokens(verdict: Verdict, template?: string): { line: string; tokens: Record; };