ai-sdk-guardrails
Version:
Input and output guardrails middleware for Vercel AI SDK.
221 lines (215 loc) • 10.6 kB
TypeScript
import { Telemetry } from 'ai';
import { G as GuardrailResult, e as GuardrailExecutionSummary } from '../types-C7t6e3EI.js';
import { G as GovernanceAgentIdentity, a as PolicyDecision, b as GovernanceDelegationContext, c as PolicyDecisionMetadata, d as GuardrailGovernanceOptions, P as PlanRiskAssessment, A as AgentActionRiskClass } from '../peer-CxUvjCqU.js';
export { e as AgentInputProvenance, _ as __setAutotelAgentModule } from '../peer-CxUvjCqU.js';
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 };