UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

211 lines • 10.1 kB
/** * Guardrails on the agent loop's own seams: an {@link InputProcessor} that scans (and redacts) every * prompt — including the tool results riding in it — an {@link OutputProcessor} that scans every * answer and the tool calls it asks for, a tool-handler wrapper, and a tool-definition screen. * * Built only on the processor SPI, so it is a consumer of the loop rather than a part of it: the * same rules and detectors run standalone through `scan` for traffic that never meets the loop. */ import type { InputProcessor, OutputProcessor } from '../spi/processors.js'; import type { ToolHandler } from '../spi/tool.js'; import type { Actor } from '../types.js'; import { type ToolDefinitionText } from './detectors/tool-poisoning.js'; import { type GuardHit, type ScanOptions, type ScanResult } from './engine.js'; import type { Finding, GuardContext, GuardrailAction, GuardrailRule, GuardrailStage, PiiType, SecretType, Segment } from './types.js'; import { Vault } from './vault.js'; /** What a rule's `when` and a custom detector see when the guardrails run inside the loop. */ export interface AgentGuardContext extends GuardContext { /** Undefined only for a tool definition screened outside any turn. */ threadId?: string; actor?: Actor; agentName?: string; /** Model step within the run; undefined outside a processor. */ step?: number; /** The tool server a screened definition came from (e.g. the MCP server's name). */ server?: string; } type Stages = readonly GuardrailStage[]; export interface PiiShorthand { action: GuardrailAction; types?: readonly PiiType[]; /** * Put the original values back into the answer and into tool arguments (the model only sees * placeholders). Covers values from the prompt and from tool results. Default true. */ restore?: boolean; stages?: Stages; } export interface SecretsShorthand { action: GuardrailAction; types?: readonly SecretType[]; stages?: Stages; } export interface InjectionShorthand { /** Default `block`. */ action?: GuardrailAction; /** Noisy-OR score (0-1) at which the heuristics fire. Default 0.5. */ threshold?: number; stages?: Stages; } export interface ToolPoisoningShorthand { /** `block` (default) refuses the tool in {@link Guardrails.screenTool}; `log` only reports it. */ action?: 'block' | 'log'; threshold?: number; } /** One decision worth auditing. Never carries the matched values themselves. */ export interface GuardrailEvent { stage: GuardrailStage; /** The effective action (`approve` is reported as it was configured, and enforced as a block). */ action: GuardrailAction; decisive?: { id: string; name: string; message: string; }; /** Hits in the newest content only — history re-sent every step is not reported again. */ hits: GuardrailEventHit[]; redactions: number; latencyMs: number; context: AgentGuardContext; } export type GuardrailEventHit = Omit<GuardHit, 'values'> & { /** {@link GuardrailsOptions.fingerprint} of each distinct value; empty without one. */ fingerprints: string[]; }; /** Where a thread's reversible placeholders live between the prompt and the answer. */ export interface VaultStore { load(threadId: string): Vault | undefined | Promise<Vault | undefined>; save(threadId: string, vault: Vault): void | Promise<void>; } /** * The default {@link VaultStore}: this process's memory, bounded to the most recent threads. A run * resumed in ANOTHER process finds no vault, so its placeholders reach the reader unrestored — the * values still never reach the model. Supply a shared store (with {@link Vault.toJSON}) to restore * across processes; it then holds the raw values. */ export declare class InMemoryVaultStore implements VaultStore { private readonly maxThreads; private readonly vaults; constructor(maxThreads?: number); load(threadId: string): Vault | undefined; save(threadId: string, vault: Vault): void; } export type GuardrailRulesSource = readonly GuardrailRule<AgentGuardContext>[] | ((ctx: AgentGuardContext) => readonly GuardrailRule<AgentGuardContext>[] | Promise<readonly GuardrailRule<AgentGuardContext>[]>); export interface GuardrailsOptions { /** PII (email, phone, CPF/CNPJ, SSN, cards with Luhn, IBAN, IPv4). */ pii?: GuardrailAction | PiiShorthand; /** API keys, tokens and private keys. */ secrets?: GuardrailAction | SecretsShorthand; /** Prompt-injection / jailbreak heuristics. */ injection?: GuardrailAction | InjectionShorthand; /** Screen tool definitions for hidden instructions — see {@link Guardrails.screenTool}. */ toolPoisoning?: boolean | ToolPoisoningShorthand; /** * More rules, evaluated alongside the shorthands. A function is called per scan with the turn's * context, which is the seam for per-tenant policy: look the tenant's rules up by `ctx.actor`. */ rules?: GuardrailRulesSource; /** Audit hook. Awaited; a throw is swallowed so an audit sink can never change a decision. */ onEvent?: (event: GuardrailEvent) => void | Promise<void>; /** Keyed hash (or any stable digest) of a matched value, reported on events instead of it. */ fingerprint?: (value: string) => string; /** Per-stage refusal messages for rules that set none. */ messages?: ScanOptions['messages']; /** Where reversible placeholders live between prompt and answer. Default {@link InMemoryVaultStore}. */ vaults?: VaultStore; /** * Keep the turn streaming: the output processor declares `incremental` with this lookback, so * the loop releases a prefix while holding the last `lookbackChars` characters. `false` gates the * whole answer instead. Default `{ lookbackChars: 256 }` — wide enough for every built-in * pattern and placeholder; widen it for custom patterns that match longer text. */ incremental?: false | { lookbackChars?: number; }; /** Processor name (user-visible on a refusal). Default `guardrails`. */ name?: string; } /** A guardrail refused content the loop cannot rewrite around: a prompt, or a tool call. */ export declare class GuardrailBlockedError extends Error { readonly stage: GuardrailStage; /** The message for the person — the rule's own, or the stage default. */ readonly userMessage: string; readonly rule?: { id: string; name: string; } | undefined; constructor(stage: GuardrailStage, /** The message for the person — the rule's own, or the stage default. */ userMessage: string, rule?: { id: string; name: string; } | undefined); } /** The rules the shorthand options stand for. Exported so a caller can inspect or extend them. */ export declare function shorthandRules(options: GuardrailsOptions): GuardrailRule<AgentGuardContext>[]; /** * Guardrails for the agent loop. Register the two processors, optionally wrap tool handlers, and * hand {@link screenTool} to whatever imports remote tool definitions: * * ```ts * const guardrails = createGuardrails({ pii: 'redact', secrets: 'block', injection: { threshold: 0.6 }, toolPoisoning: true }); * // config/agent.ts * defineConfig({ inputProcessors: [guardrails.input], outputProcessors: [guardrails.output], … }); * ``` */ export declare class Guardrails { private readonly options; /** Scans every prompt; redacts in place, withholds tool results, throws on a blocked prompt. */ readonly input: InputProcessor; /** Scans every answer and the tool calls it asks for; redacts, restores placeholders, or rejects. */ readonly output: OutputProcessor; private readonly shorthand; private readonly vaults; /** Per thread: hits already reported this turn, so re-scans of the same content stay quiet. */ private readonly reported; constructor(options?: GuardrailsOptions); /** Every rule in force for `ctx`: the shorthands, then {@link GuardrailsOptions.rules}. */ rulesFor(ctx: AgentGuardContext): Promise<GuardrailRule<AgentGuardContext>[]>; /** * Scans arbitrary segments at one stage with the configured rules, reporting through `onEvent`. * What the processors use; also the entry point for traffic that never meets the loop. */ scan(ctx: AgentGuardContext, segments: readonly Segment[], vault?: Vault): Promise<ScanResult>; /** * Screens one tool definition (name, title, description, and every description in its input * schema) for hidden instructions. `allowed: false` means a `block`/`approve` rule fired — leave * the tool out of the catalog. Shaped to be handed straight to an MCP importer's screen hook. */ screenTool(tool: ToolDefinitionText & { server?: string; }): Promise<{ allowed: true; } | { allowed: false; reason: string; }>; /** * Wraps a tool handler so its arguments are guarded where the loop cannot rewrite them: the * thread's placeholders are restored (the model only ever saw `[EMAIL_1]`, the tool needs the * address), then the `tool_args` rules run — `redact` rewrites what the tool receives, `block` * throws a {@link GuardrailBlockedError}, which the model reads as the tool's failure. */ wrapTool<I, O>(toolName: string, handler: ToolHandler<I, O>): ToolHandler<I, O>; private contextOf; private processInput; private guardToolResult; private processOutput; private report; } /** Shorthand for `new Guardrails(options)`. */ export declare function createGuardrails(options?: GuardrailsOptions): Guardrails; /** * The tool-poisoning screen alone — no rules, no events. `score` is the noisy-OR of the signals * that fired; `findings` is empty below `threshold` (default 0.5). */ export declare function screenToolDefinition(tool: ToolDefinitionText, threshold?: number): { score: number; findings: Finding[]; }; export {}; //# sourceMappingURL=agent.d.ts.map