UNPKG

ai-sdk-guardrails

Version:

Input and output guardrails middleware for Vercel AI SDK.

221 lines (215 loc) 10.6 kB
import { Telemetry } from 'ai'; import { G as GuardrailResult, e as GuardrailExecutionSummary } from '../types-C7t6e3EI.cjs'; import { G as GovernanceAgentIdentity, a as PolicyDecision, b as GovernanceDelegationContext, c as PolicyDecisionMetadata, d as GuardrailGovernanceOptions, P as PlanRiskAssessment, A as AgentActionRiskClass } from '../peer-BxTI4phw.cjs'; export { e as AgentInputProvenance, _ as __setAutotelAgentModule } from '../peer-BxTI4phw.cjs'; import '@ai-sdk/provider'; /** * Pure SAIF policy mappers — the guardrail-result → policy-decision mapping, * unit-tested in isolation. No I/O, no optional-peer lifecycle; just data shape * transforms over {@link ../types.GuardrailResult} and the peer type vocabulary. */ /** Map a guardrail result to a 0..1 risk score: confidence first, else severity. */ declare function severityToRiskScore(result: GuardrailResult): number | undefined; /** Best-available human name for the guardrail that produced a result. */ declare function guardrailNameOf(result: GuardrailResult): string; /** * The single constructor for a `recordPolicyDecision` payload. Every emit path * funnels through here, so the `eventKind` constant and the `policy` sub-shape * live in exactly one place (and the unit test that covers it covers them all). */ declare function policyDecision(input: { action: string; agent: GovernanceAgentIdentity; decision: PolicyDecision; resource?: string; riskScore?: number; reason?: string; category?: string; delegation?: GovernanceDelegationContext; reasoningSummary?: string; }): PolicyDecisionMetadata; /** * Map a blocked guardrail result to a `recordPolicyDecision` payload. A block is * a deterministic policy-engine **deny** (SAIF Principle 2 / Layer 1). */ declare function toPolicyDecisionMetadata(result: GuardrailResult, options: GuardrailGovernanceOptions): PolicyDecisionMetadata; /** Map a v7 `toolApproval` status to a policy decision. */ declare function approvalStatusToPolicyDecision(statusType: 'approved' | 'denied' | 'user-approval' | 'not-applicable'): PolicyDecision | undefined; /** * `ai-sdk-guardrails/governance` — bridge guardrail outcomes into * **autotel-genai**'s agent-governance signals, structured around Google's * *Secure AI Agents* (SAIF) three principles: * * 1. **Well-defined human controllers** — records the controlling user * (`recordControllerId`), the provenance of untrusted input * (`recordInputProvenance`), and human-in-the-loop approval outcomes * (`recordHumanApproval`). * 2. **Limited powers** — every block is emitted as a deterministic policy * decision (`recordPolicyDecision` with `decision: 'deny'`), the canonical * record of a runtime policy engine refusing an action. * 3. **Observable actions** — all of the above land on the active OpenTelemetry * GenAI span, so guardrail decisions live in the *same* trace tree as the * model calls they guard. * * `autotel-genai` is an **optional peer**: the {@link ./peer} layer lazily * imports it and silently no-ops when it (or an active trace context) is absent. * Observability must never crash the call it observes, so every emit is * best-effort. * * This module is the barrel + the four product factories. The shared substrate * lives in {@link ./peer} (optional-peer types + lifecycle) and the pure SAIF * mapping in {@link ./mappers}. * * ```ts * import { withGuardrails, promptInjectionDetector, sensitiveDataFilter } from 'ai-sdk-guardrails'; * import { guardrailGovernance } from 'ai-sdk-guardrails/governance'; * * const gov = guardrailGovernance({ * agent: { id: 'support-agent', model: 'gpt-4o' }, * controllerId: user.id, * }); * * const model = withGuardrails({ model: baseModel, * inputGuardrails: [promptInjectionDetector()], * outputGuardrails: [sensitiveDataFilter()], * onInputBlocked: gov.onInputBlocked, * onOutputBlocked: gov.onOutputBlocked, * }); * ``` */ interface GuardrailGovernanceHooks { onInputBlocked: (summary: GuardrailExecutionSummary) => void; onOutputBlocked: (summary: GuardrailExecutionSummary) => void; } /** * Build `onInputBlocked` / `onOutputBlocked` hooks that emit each guardrail * block as an autotel-genai policy decision (plus controller + provenance on * input). Spread the result into a `withGuardrails` config. */ declare function guardrailGovernance(options: GuardrailGovernanceOptions): GuardrailGovernanceHooks; /** Decision info handed to `guardrailApproval`'s `onDecision` hook. */ interface ApprovalDecisionInfo { toolName: string; /** The SDK tool-call id, threaded through to `recordHumanApproval`. */ toolCallId?: string; status: { type: 'approved' | 'denied' | 'user-approval' | 'not-applicable'; }; guardrail?: string; result?: GuardrailResult; } /** * Build an `onDecision` callback for {@link guardrailApproval}, recording each * tool-approval outcome as a policy decision and — for human-in-the-loop * (`user-approval`) and `denied` — a `recordHumanApproval` signal (SAIF * Principle 1). When {@link GuardrailGovernanceOptions.toolRiskClass} is set, the * gated tool's action risk class is recorded too (SAIF Principle 3), for every * tool — including those no guardrail governs. The recorded `approved` reflects * the decision *at gate time*: `denied`/`user-approval` halt the call, so * `approved` is `false`. */ declare function guardrailGovernanceApproval(options: GuardrailGovernanceOptions): (info: ApprovalDecisionInfo) => void; /** * Build a **native AI SDK `Telemetry` integration** that records SAIF * agent-governance signals on the SDK's own tool-execution lifecycle. Drop it * into the v7 `telemetry.integrations` slot — the single, SDK-canonical place * for observability — instead of the library's bespoke injected-`Tracer` path: * * ```ts * import { ToolLoopAgent } from 'ai'; * import { OpenTelemetry } from '@ai-sdk/otel'; * import { guardrailTelemetry } from 'ai-sdk-guardrails/governance'; * * const agent = new ToolLoopAgent({ * model, * tools, * telemetry: { * integrations: [ * new OpenTelemetry({ tracer }), // creates the GenAI span tree * guardrailTelemetry({ // rides it: SAIF signals per action * agent: { id: 'support-agent', model: 'gpt-4o' }, * toolRiskClass: (t) => (t === 'executeSQL' ? 'destructive' : undefined), * }), * ], * }, * }); * ``` * * For every tool the agent actually executes it records the action's risk class * (SAIF Principle 3) and an `observe` policy decision, attributed to the agent * identity, onto the active GenAI span. Because it implements `ai`'s `Telemetry` * interface, no adapter is needed — and it composes with any other integration * (e.g. `@ai-sdk/otel`'s `OpenTelemetry`) registered alongside it. * * Best-effort: no-ops when the `autotel-genai` optional peer (or an active span) * is absent. Pair it with explicit `guardrailGovernance(...).onInputBlocked` / * `.onOutputBlocked` hooks on `withGuardrails` to emit the block decisions the * SDK lifecycle cannot see. */ declare function guardrailTelemetry(options: GuardrailGovernanceOptions): Telemetry; /** * Record a plan-risk assessment on the active span (SAIF Layer-2). Best-effort: * no-ops when the optional peer or an active span is absent. Used by the * `planRiskGuardrail` to land `agent.plan.risk.*` attributes alongside the model * call. Set `emitSecurityEvent` to also emit `llm.plan.risk.elevated` for * non-`low` verdicts. */ declare function recordPlanRisk(assessment: PlanRiskAssessment, toolSequence: string[], options?: { emitSecurityEvent?: boolean; }): void; /** Thrown when a guarded tool is invoked without the scopes it requires. */ declare class ToolScopeDeniedError extends Error { readonly toolName: string; readonly missingScopes: string[]; constructor(toolName: string, missingScopes: string[]); } interface GuardedToolOptions { /** The distinct agent identity invoking the tool (SAIF Principle 1). */ agent: GovernanceAgentIdentity; /** Name of the tool, used as the policy `action`/`resource`. */ toolName: string; /** Scopes the tool requires to run. */ requiredScopes?: string[]; /** * Scopes currently granted to the agent. Any {@link requiredScopes} not in * this set is a least-privilege violation → the call is denied. */ grantedScopes?: string[]; /** Action risk class recorded for the call (SAIF Principle 3). */ riskClass?: AgentActionRiskClass; /** Controlling user id, recorded on a denial. */ controllerId?: string; hashSalt?: string; /** Delegation context for a multi-agent handoff (authority lineage). */ delegation?: GovernanceDelegationContext; onMissingContext?: 'warn' | 'skip' | 'throw'; } type GuardedExecute = (...args: any[]) => any; /** * Wrap an AI SDK tool with **deterministic least-privilege enforcement** (SAIF * Principle 2 — agent powers must be limited). Before the tool runs, the wrapper * checks that {@link GuardedToolOptions.grantedScopes} covers every * {@link GuardedToolOptions.requiredScopes}; if not, it records a `deny` policy * decision and throws {@link ToolScopeDeniedError} — the tool never executes. * On a permitted call it records a `permit` decision and the action risk class. * * The scope check is pure and works standalone; the canonical autotel-genai * emission (policy decision + risk class + delegation lineage) is best-effort and * no-ops when the optional peer (or an active span) is absent. * * ```ts * const tools = { * transferFunds: withGuardedTool(transferFundsTool, { * agent: { id: 'payments-agent' }, * toolName: 'transferFunds', * requiredScopes: ['payments:write'], * grantedScopes: session.scopes, * riskClass: 'financial', * }), * }; * ``` */ declare function withGuardedTool<T extends { execute?: GuardedExecute; }>(tool: T, options: GuardedToolOptions): T; export { AgentActionRiskClass, type ApprovalDecisionInfo, GovernanceAgentIdentity, GovernanceDelegationContext, type GuardedToolOptions, type GuardrailGovernanceHooks, GuardrailGovernanceOptions, PlanRiskAssessment, PolicyDecision, PolicyDecisionMetadata, ToolScopeDeniedError, approvalStatusToPolicyDecision, guardrailGovernance, guardrailGovernanceApproval, guardrailNameOf, guardrailTelemetry, policyDecision, recordPlanRisk, severityToRiskScore, toPolicyDecisionMetadata, withGuardedTool };