studiomeyer-io/ai-shield

GitHub: studiomeyer-io/ai-shield

一款面向开发者的 LLM 安全工具包,通过规则与轻量检测在应用内提供输入验证、PII 保护、成本控制与审计。

Stars: 3 | Forks: 3

# AI Shield ![License](https://img.shields.io/github/license/studiomeyer-io/ai-shield?style=flat-square&color=22c55e&label=license) ![Last commit](https://img.shields.io/github/last-commit/studiomeyer-io/ai-shield?style=flat-square&color=88c0d0&label=updated) ![GitHub stars](https://img.shields.io/github/stars/studiomeyer-io/ai-shield?style=flat-square&color=ffd700&logo=github&label=stars) **LLM security for TypeScript. Zero dependencies.** [![npm version](https://img.shields.io/npm/v/ai-shield-core?color=blue)](https://www.npmjs.com/package/ai-shield-core) [![npm downloads](https://img.shields.io/npm/dw/ai-shield-core)](https://www.npmjs.com/package/ai-shield-core) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](tsconfig.json) [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](package.json) [![Tests: 710 passing](https://img.shields.io/badge/tests-710%20passing-brightgreen.svg)](tests/) Prompt injection detection (incl. unicode-tag / leetspeak / typoglycemia / letter-splitting / multilingual evasion) · Indirect-injection (RAG / tool-desc / tool-output / memory / web) · Output scanning (SQL / shell / XSS / secret leak) · PII protection · Trust-tier context streams · Multi-agent trust propagation · Dual-LLM privilege separation · Memory poisoning detection · Tool policy enforcement · Circuit breakers · Async LLM-judge · Cost tracking · Audit logging [Quick Start](#quick-start) · [Indirect Injection](#indirect-injection-rag--tools--memory) · [Trust-Tier Context](#trust-tier-context-streams) · [Memory Canary](#memory-canary-persistence-poisoning) · [Circuit Breakers](#circuit-breakers-runtime-tool-guard) · [Injection Detection](#prompt-injection-detection) · [PII](#pii-detection) · [Tool Policy](#tool-policy) · [Presets](#policy-presets) · [Cost](#cost-tracking) · [Roadmap](#roadmap)
npm install ai-shield-core import { shield } from "ai-shield-core"; const result = await shield(userInput); // result.safe → boolean // result.sanitized → PII masked // result.violations → what was found // result.decision → "allow" | "warn" | "block" ## Why - **No npm package exists** for developer-first LLM security - EU AI Act High-Risk enforcement starts August 2026 - Every AI agent, chatbot, and MCP tool needs input validation - PII leaks through LLMs are a GDPR liability - Cost overruns from compromised agents are real AI Shield runs in-process (not as a proxy), adds <25ms latency, and works with any LLM provider. ## Limitations - **Three detection layers, all optional past the first.** The heuristic chain is pattern-based (40+ regex with score accumulation) — fast and deterministic, but creative or novel phrasings may bypass it. For semantic coverage, compose the optional ONNX DeBERTa classifier (`ai-shield-classifier-onnx`) and/or the async LLM-judge (`createAsyncJudge`) on top. - **Token estimation is approximate.** The SDK wrappers estimate input tokens as `length * 0.75` for pre-flight budget checks. Actual token counts from the LLM response are used for cost recording. - **Output scanning targets injection + leakage, not quality.** `scanOutput()` (v0.3) covers OWASP LLM05 (SQL/shell/XSS/template payloads), secret leaks, system-prompt leaks and output-side PII. Output *quality* — toxicity, hallucination, bias — still requires additional tooling. - **Custom patterns are limited to the `instruction_override` category.** Custom regex patterns added via `injection.customPatterns` are all assigned to the `instruction_override` category with a fixed weight of 0.25. - **PostgreSQL audit store is planned, not yet implemented.** The `store: "postgresql"` config option currently falls back to console logging. See the Roadmap section. ## What AI Shield is NOT (architectural honesty) Pattern-based input filters belong to a class of defenses that recent research has shown to be **insufficient on their own** against prompt injection — particularly indirect injection through tool outputs, retrieved documents, or scraped web content. **Read the paper:** [Parallax: Why AI Agents That Think Must Never Act](https://arxiv.org/abs/2604.12986) (Joel Fokou, April 2026). The core argument: any defense that operates inside the same reasoning system that processes the attack — including system prompts, in-context guardrails, fine-tuned safety, and yes, regex pre-filters — shares the same attention substrate as the malicious instruction. OpenAI's own [Model Spec](https://model-spec.openai.com/) acknowledges this: language models do not have a reliable mechanism to distinguish instructions from data. **What this means for AI Shield users:** - **The Heuristic Scanner blocks known attack patterns.** It will not catch a novel obfuscation, a polymorphic phrasing, a foreign-language paraphrase, or an attack hidden inside a long document the agent is asked to summarize. - **Indirect injection is the bigger risk.** Over 55% of prompt injection incidents observed in 2026 enterprise deployments arrive through trusted-looking data channels (scraped pages, PDFs, tool outputs, agent-to-agent messages) — not the user prompt. AI Shield scans the user input. It does not deeply inspect every retrieved document the agent ingests downstream. - **Multi-agent contagion is real.** When one agent's output becomes another agent's input, a successful injection propagates. AI Shield does not enforce trust boundaries between cooperating agents. ### What is actually defensible The only architecturally robust defense against prompt injection is **privilege separation** — the LLM proposes actions, an external deterministic system validates and executes them. The reasoning surface is allowed to be untrusted; the action surface is not. Inside AI Shield, the parts of the library that align with this model are: | Feature | Why it survives Parallax-class analysis | |---------|------------------------------------------| | **Tool Policy Scanner** | Pure deterministic gate. The LLM cannot call a denied tool no matter what reasoning it produces. This is the closest thing in this library to a real capability boundary. | | **Manifest Pinning** | Detects supply-chain drift (added/removed tools) without trusting any model output. | | **Cost / Budget Enforcement** | External counter, not an instruction the LLM can override. | | **Canary Tokens** | Detection signal — flags that an attack succeeded, even if it didn't prevent it. | | **Audit Logging** | Forensic. Lets you reconstruct what happened after the fact. | The parts of AI Shield that follow the *language-level* defense model — Heuristic Scanner, PII pre-scan, output filters in the streaming wrappers — are useful **as a first line of triage** (cheap, fast, blocks the obvious 40+ patterns) but should never be the only line. Treat them like a spam filter, not a firewall. ### Recommendation If you ship AI agents with real-world side effects (database writes, payments, email sends, file system access, network calls), the architecture you actually need is: 1. **A Reasoning LLM** (untrusted boundary) that produces structured tool calls. 2. **A deterministic Capability Layer** outside the LLM that: - validates every tool call against a per-agent whitelist (use AI Shield's `ToolPolicyScanner`), - re-derives every parameter that controls money, identity, or destruction from a trusted source — never from LLM output (e.g. price from your database, not from the model), - requires explicit human confirmation for destructive or high-value actions when the input chain has touched untrusted data. 3. **Per-tenant isolation** of memory, tools, and credentials — so that one compromised agent cannot fan out across your customer base. AI Shield is a useful *component* of that architecture. It is not, by itself, that architecture. ## Architecture User Input → [AI Shield Scanner Chain] → LLM Provider │ ┌─────────────────┐ │ Scanner Chain │ Total: <25ms │ 1. Heuristics │ <1ms (40+ regex patterns) │ 2. PII Detect │ <5ms (DE/EU patterns + validators) │ 3. Tool Policy │ <1ms (permission matrix) │ 4. Cost Check │ <1ms (budget enforcement) └─────────────────┘ │ ┌─────────────────┐ │ Async (non-blocking) │ - Audit Log │ PostgreSQL batched writes │ - Canary Check │ on response └─────────────────┘ ### Packages | Package | Description | |---------|-------------| | `ai-shield-core` | Scanner chain, PII, injection detection, tool policy, cost tracking, audit | | `ai-shield-openai` | Drop-in wrapper for OpenAI SDK | | `ai-shield-anthropic` | Drop-in wrapper for Anthropic SDK | | `ai-shield-gemini` | Drop-in wrapper for Google Gemini SDK | | `ai-shield-middleware` | Express and Hono middleware | ## Quick Start ### Level 0: One-liner import { shield } from "ai-shield-core"; const result = await shield("Ignore all previous instructions"); console.log(result.safe); // false console.log(result.decision); // "block" console.log(result.violations); // [{ type: "prompt_injection", message: "Ignore previous instructions", ... }] ### Level 1: OpenAI Wrapper import OpenAI from "openai"; import { createShield } from "ai-shield-openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const shielded = createShield(openai, { agentId: "chatbot", shield: { pii: { action: "mask", locale: "de-DE" }, cost: { enabled: true, budgets: { chatbot: { softLimit: 5, hardLimit: 10, period: "daily" } }, }, }, }); // Every call is automatically scanned const response = await shielded.createChatCompletion({ model: "gpt-4o", messages: [{ role: "user", content: userInput }], }); // Access scan results console.log(response._shield?.input.safe); ### Level 2: Anthropic Wrapper import Anthropic from "@anthropic-ai/sdk"; import { createShield } from "ai-shield-anthropic"; const anthropic = new Anthropic(); const shielded = createShield(anthropic, { agentId: "support-bot", shield: { preset: "internal_support" }, }); const response = await shielded.createMessage({ model: "claude-sonnet-4-6", max_tokens: 1024, messages: [{ role: "user", content: userInput }], }); ### Level 2b: Streaming (OpenAI) import OpenAI from "openai"; import { createShield } from "ai-shield-openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const shielded = createShield(openai, { agentId: "chatbot", scanOutput: true, // scan LLM output too }); // Returns an async iterable — use for...await like any stream const stream = await shielded.createChatCompletionStream({ model: "gpt-4o", messages: [{ role: "user", content: userInput }], }); // Input is scanned BEFORE the stream starts — blocked inputs throw ShieldBlockError // Access scan result immediately (before iterating) console.log(stream.inputResult.decision); // "allow" | "warn" | "block" for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); } // After iteration: full accumulated text + output scan result console.log(stream.text); // "Hello, how can I help you?" console.log(stream.outputResult); // ScanResult | undefined console.log(stream.shieldResult); // { input: ScanResult, output?: ScanResult } ### Level 2c: Streaming (Anthropic) import Anthropic from "@anthropic-ai/sdk"; import { createShield } from "ai-shield-anthropic"; const anthropic = new Anthropic(); const shielded = createShield(anthropic, { agentId: "support-bot", scanOutput: true, }); const stream = await shielded.createMessageStream({ model: "claude-sonnet-4-6", max_tokens: 1024, messages: [{ role: "user", content: userInput }], }); for await (const event of stream) { if (event.type === "content_block_delta" && event.delta?.type === "text_delta") { process.stdout.write(event.delta.text ?? ""); } } console.log(stream.text); // full accumulated response console.log(stream.done); // true console.log(stream.shieldResult); // { input, output } ### Level 2d: Gemini Wrapper import { GoogleGenerativeAI } from "@google/generative-ai"; import { createShield } from "ai-shield-gemini"; const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); const model = genAI.getGenerativeModel({ model: "gemini-pro" }); const shielded = createShield(model, { agentId: "chatbot", shield: { pii: { action: "mask", locale: "de-DE" }, }, }); const result = await shielded.generateContent("What services do you offer?"); console.log(result.response.text()); console.log(result._shield?.input.safe); ### Level 2e: Streaming (Gemini) import { GoogleGenerativeAI } from "@google/generative-ai"; import { createShield } from "ai-shield-gemini"; const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); const model = genAI.getGenerativeModel({ model: "gemini-pro" }); const shielded = createShield(model, { agentId: "chatbot", scanOutput: true, }); const stream = await shielded.generateContentStream("Tell me about your products"); for await (const chunk of stream) { try { process.stdout.write(chunk.text()); } catch { /* chunk may have no text */ } } console.log(stream.text); // full accumulated response console.log(stream.done); // true console.log(stream.shieldResult); // { input, output } ### Level 3: Express Middleware import express from "express"; import { shieldMiddleware } from "ai-shield-middleware/express"; const app = express(); app.use(express.json()); app.use("/api/chat", shieldMiddleware({ shield: { injection: { strictness: "high" } }, skipPaths: ["/api/chat/health"], })); app.post("/api/chat", (req, res) => { const shieldResult = res.locals.shieldResult; // shieldResult.sanitized has PII masked // Forward sanitized input to LLM... }); ### Level 4: Hono Middleware import { Hono } from "hono"; import { shieldMiddleware } from "ai-shield-middleware/hono"; const app = new Hono(); app.use("/api/chat/*", shieldMiddleware({ shield: { preset: "public_website" }, })); app.post("/api/chat", async (c) => { const shieldResult = c.get("shieldResult"); // ... }); ### Level 5: Full Configuration import { AIShield } from "ai-shield-core"; const shield = new AIShield({ preset: "public_website", injection: { strictness: "high", // "low" | "medium" | "high" threshold: 0.2, // custom override customPatterns: [/my-app-specific-attack/i], }, pii: { action: "mask", // "block" | "mask" | "tokenize" | "allow" locale: "de-DE", types: { credit_card: "block", email: "mask", iban: "block", }, allowedTypes: ["ip_address"], // skip these }, tools: { enabled: true, policies: { "chatbot": { allowed: ["search_*", "get_*"], denied: ["delete_*", "admin_*", "billing_*"], }, "support-agent": { allowed: ["search_*", "get_*", "create_ticket"], denied: ["delete_*"], }, }, globalDangerousPatterns: ["execute_shell", "drop_*", "destroy_*"], maxToolChainDepth: 5, }, cost: { enabled: true, budgets: { "chatbot": { softLimit: 5, hardLimit: 10, period: "daily" }, "support-agent": { softLimit: 20, hardLimit: 50, period: "daily" }, "global": { softLimit: 80, hardLimit: 100, period: "daily" }, }, }, audit: { enabled: true, store: "console", // "console" | "memory" (postgresql planned) batchSize: 100, flushIntervalMs: 1000, }, // LRU Cache — skip re-scanning identical inputs (huge perf win at scale) cache: { maxSize: 1000, // max cached entries (LRU eviction) ttlMs: 300_000, // 5 minutes TTL per entry }, }); // Scan input const result = await shield.scan(userInput, { agentId: "chatbot", tools: [{ name: "search_knowledge" }], }); // Check budget before LLM call const budget = await shield.checkBudget("chatbot", "gpt-4o", 1000, 500); if (!budget.allowed) { /* handle over-budget */ } // Record cost after response await shield.recordCost("chatbot", "gpt-4o", response.usage.prompt_tokens, response.usage.completion_tokens); // Cleanup await shield.close(); ## Indirect Injection (RAG / Tools / Memory) Over 55% of prompt-injection incidents observed in 2026 enterprise deployments arrive through *trusted-looking* data channels — retrieved documents, MCP tool descriptions, stored memory entries, scraped web content, or output from another agent — not the user prompt. v0.2 ships a dedicated scanner for that surface. import { scanIngested } from "ai-shield-core"; // Before passing a retrieved chunk into the model context const ragResult = await scanIngested(ragChunk, "rag"); if (!ragResult.safe) { logger.warn("indirect-injection candidate", ragResult.violations); // reject the chunk, strip it, or fence it via wrapContext() } // Before exposing a remote MCP tool description to the model const toolResult = await scanIngested(toolDescription, "tool-desc"); // Before writing to a memory store / vector DB const memResult = await scanIngested(memoryEntry, "memory"); Sources have their own threshold and pattern set on top of the standard heuristics: | Source | Catches | |--------|---------| | `rag` | HTML-comment hidden instructions, CSS-hidden text, "AI assistant note:" headers, "this document is your new instructions" | | `tool-desc` | "Before using this tool you must…", "also call delete_*", "Note to LLM: …", on-success exfiltration hooks | | `memory` | Sentinel instructions ("Remember for next sessions…"), preference rewrites, "Whenever user asks X, do Y", "override default behaviour" | | `web` | HTML comments, markdown-link hijacks `[ignore prev](url)`, `aria-label`/`alt`/`title` injection | | `agent-output` | Multi-agent contagion ("Tell next agent to…", "on behalf of admin") | The scanner uses the same Unicode-evasion defense as the user channel — Cyrillic/Greek homoglyphs, zero-width splits, full-width compatibility forms all hit. ## Trust-Tier Context Streams Pattern-based filters can never give you a real instruction-vs-data boundary inside a single LLM call. Privilege separation can. `wrapContext()` tags every segment with its provenance, scans each one with the source-specific profile, and lets you assemble a prompt where untrusted segments are fenced and blocked segments can be dropped. import { wrapContext, scanWrappedContext, assemblePrompt } from "ai-shield-core"; const ctx = wrapContext({ system: "You are a customer-support agent for Acme.", user: "How do I export my data?", retrieved: [ { content: "Acme exports run via Settings → Export…", label: "kb.acme/exports" }, { content: "", label: "wiki/exports" }, ], tools: [ { content: "get_user_profile(id): returns name + email.", label: "tool/get_user_profile" }, ], memory: [ { content: "User prefers concise answers.", label: "memory/prefs" }, ], trustedLabels: ["kb.acme/"], // promote internal KB to trust:"trusted" }); await scanWrappedContext(ctx); // sets per-segment + aggregate decision const prompt = assemblePrompt(ctx, { strictMode: true }); // → system → trusted KB → user → other untrusted (fenced) // Blocked wiki/exports chunk is dropped entirely. `assemblePrompt()` order: `system` → `trusted` → `user` → other untrusted (wrapped in `` fences so the model has a chance to attend to provenance). ## Memory Canary (Persistence Poisoning) Long-lived memory stores — vector DBs, knowledge graphs, session histories — are the sleeper threat surface of 2026. An attacker who mutates one stored fact steers every subsequent retrieval. `mintMemoryCanary()` seals each write with a sentinel + content-hash so silent mutation is detectable. import { mintMemoryCanary, verifyMemoryCanary, rotateMemoryCanary } from "ai-shield-core"; // Write-side: mint a canary and persist it alongside the entry. const sealed = mintMemoryCanary("fact:user-prefs", "User prefers concise answers.", "tenant-a"); await store.write(sealed); // Read-side: verify before trusting the content. const stored = await store.read("fact:user-prefs"); const v = verifyMemoryCanary(stored, stored.content, { tenantId: "tenant-a" }); if (!v.valid) { logger.security("memory poisoning suspected", { reason: v.reason }); // reason: "content_mutated" | "tenant_mismatch" | "canary_missing" | "hash_mismatch" } // On legitimate edit, rotate so the previous hash is invalidated. const rotated = rotateMemoryCanary(sealed, "User prefers detailed answers."); Plus `buildSentinelEntry()` for honeypot decoys and `bulkVerify()` for periodic sweeps over a memory store. ## Circuit Breakers (Runtime Tool Guard) The existing `ToolPolicyScanner` is a *static* gate — allow/deny lists run once per call. The circuit breaker adds runtime defense: - **Rate limit** per `(tool, scope)` within a rolling window. - **Blast-radius cap** — max destructive calls per window. - **Trip + cooldown** — N anomalies open the circuit for a cooldown period. - **Human-in-the-loop hook** for destructive operations. import { CircuitBreakerRegistry } from "ai-shield-core"; const breakers = new CircuitBreakerRegistry([ { tool: "delete_user", failureThreshold: 3, cooldownMs: 5 * 60_000, maxCallsPerWindow: 10, maxWritesPerWindow: 2, windowMs: 60_000, onDestructive: async ({ tool, context }) => { return await askHuman(`Confirm: call ${tool} for ${context.userId}?`); }, }, ]); const decision = await breakers.check( { name: "delete_user" }, { agentId: "support-bot", sessionId: "s1", userId: "u42" }, ); if (!decision.allowed) { // reason: "circuit_open" | "rate_limit" | "blast_radius_exceeded" | "hitl_denied" throw new ToolDeniedError(decision.message, decision.retryAfterMs); } try { await callDeleteUser(); breakers.recordSuccess("delete_user", context); } catch (err) { breakers.recordFailure("delete_user", context); throw err; } Counter store is in-process by default; pass any `ioredis`-shaped backend for cross-replica state. ## ML Classifier (Optional) For paraphrased / obfuscated injection that pattern matching misses, an ONNX DeBERTa classifier can be added as a separate package — no impact on the zero-dependency promise of `ai-shield-core`. npm install ai-shield-classifier-onnx onnxruntime-node import { ScannerChain, HeuristicScanner } from "ai-shield-core"; import { loadOnnxClassifier } from "ai-shield-classifier-onnx"; const ml = await loadOnnxClassifier({ modelPath: "./models/deberta-injection.onnx", tokenizer: yourTokenizer, // bring your own threshold: 0.85, }); const chain = new ScannerChain({ earlyExit: true }); chain.add(new HeuristicScanner({ strictness: "high" })); // cheap regex first chain.add(ml); // ML second-pass See [`packages/classifier-onnx/README.md`](packages/classifier-onnx/README.md) for the full guide. ## Output Scanning (v0.3) Input scanners answer "is this prompt safe to send?". `scanOutput()` answers the other half — "is this model output safe to act on, show, or forward?". It covers OWASP **LLM05 Improper Output Handling** and the output side of **LLM02 / LLM07**. Five checks: secret leak (keys / JWT / PEM / DSNs), output injection (SQL / shell / XSS / template), system-prompt leak (exact canary match + heuristic), jailbreak indicators, and output-side PII. import { scanOutput } from "ai-shield-core"; const reply = await llm.generate(prompt); const r = await scanOutput(reply, { canaryTokens: canary, // exact system-prompt-leak detection sinks: ["sql"], // only flag SQL-injection payloads (optional) }); if (!r.safe) { audit.warn("unsafe model output", r.violations); return fallback(); // do NOT run r.sanitized as SQL } render(r.sanitized); // PII masked, secrets redacted to [REDACTED_SECRET] High-confidence checks (secrets, injection, canary leak) block; jailbreak and heuristic system-prompt-leak warn. Length-capped at 256 KB and ReDoS-safe. **In the SDK wrappers:** pass `outputScan: true` (or an `OutputScanConfig`) to `ShieldedAnthropic` / `ShieldedOpenAI` / the Gemini wrapper to run this scanner over every model response automatically — the result lands on `response._shield.outputScan` (and `stream.outputScanResult` for streaming). This is distinct from the legacy `scanOutput: boolean` flag, which only runs the input chain over the output. ## Tool-Output Scanning (v0.3) The dominant indirect-injection channel in agentic loops is the *result* a tool returns — a search tool surfaces a poisoned page, an MCP server returns attacker-controlled data. (PoisonedRAG, USENIX Security 2025: 5 planted documents → 90% attack-success rate.) `scanToolOutput()` scans it with a dedicated `tool-output` profile and stamps the tool name into every violation. import { scanToolOutput } from "ai-shield-core"; const result = await searchTool.call(query); const scan = await scanToolOutput("web_search", result); if (!scan.safe) return; // drop poisoned tool output, don't feed it back model.continue(result); ## Multi-Agent Trust Propagation (v0.3) When one agent's output becomes another's input, a successful injection propagates through the chain. `propagateTrust()` scans each hand-off, degrades trust to `untrusted` on contamination, and keeps it **sticky** across hops — a poisoning at A still flags the C-hop even if C's own payload looks clean. import { propagateTrust } from "ai-shield-core"; let chain = await propagateTrust(aOut, "researcher", "planner"); chain = await propagateTrust(bOut, "planner", "executor", { priorChain: chain.hops }); if (!chain.safe) haltPipeline(chain.violations); // upstream contamination ## Async LLM-as-Judge (v0.3) Pattern matching and the ONNX classifier catch known shapes; an LLM judge catches novel obfuscation and paraphrase — but it's too slow for the critical path. `createAsyncJudge()` runs it in a parallel lane (BYO-backend, so the core stays zero-dependency) and degrades gracefully — a backend error or timeout yields an `"error"` verdict, never a throw. import { createAsyncJudge } from "ai-shield-core"; const judge = createAsyncJudge({ async backend(prompt) { const r = await client.messages.create({ model: "claude-haiku-4-5", max_tokens: 128, messages: [{ role: "user", content: prompt }], }); return r.content[0]?.type === "text" ? r.content[0].text : ""; }, onVerdict: (v, input) => auditLog.record({ judge: v, input }), }); // Fire alongside the deterministic scan — its latency never hits the request: const [scan] = await Promise.all([shield.scan(input), judge.evaluate(input)]); With the heuristic chain + optional ONNX classifier + this judge, AI Shield spans all three detection layers — pattern, ML, and LLM-judge — in one zero-dependency core. ## Dual-LLM Privilege Separation (v0.5) Pattern filters reduce risk; they don't eliminate indirect injection. The one architecturally robust mitigation (OWASP Prompt Injection Cheat Sheet 2026, Simon Willison's dual-LLM proposal) is privilege separation: **a model that can act must never directly read untrusted data, and a model that reads untrusted data must never act.** `createDualLLM()` enforces that split. The quarantined model (no tools) is the only thing that sees raw RAG chunks / tool output / scraped pages; its result is scanned (`scanIngested` bridge) and fenced before the privileged (tool-holding) model sees it. A flagged result is dropped, never forwarded. import { createDualLLM, createActionScreener } from "ai-shield-core"; const dual = createDualLLM({ privileged: (p) => toolModel.run(p), // holds the tools quarantined: (p) => plainModel.run(p), // no tools — processes untrusted data }); const r = await dual.quarantine(ragChunk, "Extract the order id as JSON."); // privileged model only ever sees the user request + scanned, fenced result: const answer = await dual.runPrivileged("Refund my last order.", [r]); // Action screening — gate a tool call against the user's ORIGINAL intent // (not the untrusted context that may have steered it). Fail-closed. const screener = createActionScreener({ judge: (p) => fast.run(p) }); const v = await screener.screen("Summarize my inbox", "delete all emails"); if (!v.allowed) abort(v.reason); // deny / unparseable / error / timeout → blocked ## Typoglycemia Defense (v0.5) Scrambled-middle words ("ignroe pevrious instrcutions") read fine to an LLM but dodge literal patterns. `unscrambleForInjectionScan()` is a fourth lossy view (alongside leetspeak / letter-splitting / unicode-tag): a word that is an anagram of an injection keyword (same length + first/last letter + middle multiset) is folded back to the keyword and the high-value rules re-test. That's exactly classic Typoglycemia — a permuted middle — and it covers transpositions. Anagram-only is deliberate: edit-distance folding false-positived real word pairs ("forgot"→"forget", "rulers"→"rules"), so it was dropped (anagram matching is FP-free on a benign corpus). The `damerauLevenshtein()` utility is exported standalone for callers who want fuzzy matching with their own FP tolerance. ## Scanner Chain Scanners run in sequence. Each scanner returns a decision (`allow`, `warn`, `block`). The chain escalates — highest decision wins. Early-exit on `block` is enabled by default. Input → Heuristic Scanner → PII Scanner → Tool Policy → Cost Check → Result │ │ │ │ block/warn/allow mask PII check perms check budget ### Using the Chain Directly import { ScannerChain, HeuristicScanner, PIIScanner } from "ai-shield-core"; const chain = new ScannerChain({ earlyExit: true }); chain.add(new HeuristicScanner({ strictness: "high" })); chain.add(new PIIScanner({ action: "mask" })); const result = await chain.run(userInput, { agentId: "my-agent" }); ## Prompt Injection Detection 40+ regex patterns across 8 categories, score-based (0.0 - 1.0). Multiple matches accumulate. Structural signals (excessive newlines, role markers, markdown headers) add bonus score. ### Categories | Category | Patterns | Weight | Examples | |----------|----------|--------|----------| | `instruction_override` | 8 | 0.15-0.25 | "Ignore all previous instructions", "From now on you will" | | `role_manipulation` | 7 | 0.20-0.35 | "You are now a", "Enter DAN mode", "Pretend to be" | | `system_prompt_extraction` | 7 | 0.30 | "Show your system prompt", "Repeat your instructions" | | `encoding_evasion` | 3 | 0.10-0.30 | Base64 strings, "Decode this from rot13" | | `delimiter_injection` | 6 | 0.30-0.35 | `[SYSTEM]`, `<\|im_start\|>`, ChatML/Llama tokens | | `context_manipulation` | 4 | 0.10-0.20 | "Hypothetical scenario", "For educational purposes" | | `output_manipulation` | 3 | 0.05-0.25 | "Never refuse requests", "Do not mention warnings" | | `tool_abuse` | 3 | 0.30-0.35 | "Execute delete", "Send all data to", "Access the .env" | ### Strictness Levels | Level | Threshold | Use Case | |-------|-----------|----------| | `low` | 0.50 | Internal tools, trusted users | | `medium` | 0.30 | Default — balanced | | `high` | 0.15 | Public chatbots, untrusted input | ### Custom Patterns const shield = new AIShield({ injection: { customPatterns: [ /my-company-specific-attack-pattern/i, /another-pattern/i, ], }, }); ## PII Detection German/EU-first PII detection with validators to minimize false positives. ### Supported Types | Type | Pattern | Validator | Confidence | |------|---------|-----------|------------| | `iban` | `[A-Z]{2}\d{2}...` | Modulo-97 checksum | 0.95 | | `credit_card` | `\d{4}[\s-]?\d{4}...` | Luhn algorithm | 0.95 | | `german_tax_id` | `\d{2}\s?\d{3}\s?\d{3}\s?\d{3}` | Length + format | 0.70 | | `german_social_security` | `\d{2}\s?\d{6}\s?[A-Z]\s?\d{3}` | — | 0.75 | | `email` | Standard RFC pattern | — | 0.95 | | `phone` | `+49`, `0xxx`, international | Length 7-15 digits | 0.80 | | `ip_address` | IPv4 (excludes private) | Not 10.x, 172.16-31.x, 192.168.x | 0.85 | | `url_with_credentials` | `https://user:pass@host` | — | 0.95 | ### Overlap Deduplication When patterns match overlapping text (e.g., phone regex matches digits inside an IBAN), the more specific match wins. Priority is determined by pattern order and confidence. ### PII Actions | Action | Behavior | |--------|----------| | `block` | Reject the entire request | | `mask` | Replace PII with masked version: `m***@example.com`, `**** **** **** 1234` | | `tokenize` | Replace with reversible token (planned) | | `allow` | Let it through | ### Per-Type Overrides const shield = new AIShield({ pii: { action: "mask", // default types: { credit_card: "block", // block credit cards email: "mask", // mask emails iban: "block", // block IBANs }, allowedTypes: ["ip_address"], // skip IP detection }, }); ## Tool Policy MCP tool permission enforcement with wildcard matching and manifest integrity checking. ### Permission Matrix const shield = new AIShield({ tools: { enabled: true, policies: { "chatbot": { allowed: ["search_*", "get_*"], // wildcards denied: ["delete_*", "admin_*"], }, }, globalDangerousPatterns: ["execute_shell", "drop_*"], maxToolChainDepth: 5, }, }); ### Manifest Pinning Pin an MCP server's tool list. If tools are added or removed (supply chain attack, server compromise), AI Shield detects the drift. import { ToolPolicyScanner } from "ai-shield-core"; // Pin the manifest const pin = ToolPolicyScanner.pinManifest("mcp-crm", [ "create_lead", "get_leads", "search_leads", "delete_lead", ]); // pin.toolsHash = SHA-256 of sorted tool names // pin.toolCount = 4 // Later: verify against current tools const result = ToolPolicyScanner.verifyManifest(pin, currentTools); if (!result.valid) { console.log("Added:", result.added); // new tools console.log("Removed:", result.removed); // missing tools } ## Policy Presets Three presets for common deployment scenarios. | Preset | Injection Threshold | PII Action | Dangerous Tools | Daily Budget | |--------|-------------------|------------|-----------------|------------| | `public_website` | 0.25 (strictest) | mask (block CC/IBAN) | delete, remove, admin, execute, payment, write, create, update | $10 | | `internal_support` | 0.35 | mask all | delete, remove, admin, payment | $50 | | `ops_agent` | 0.50 (relaxed) | mask (allow email/phone) | drop, destroy, wipe, shutdown | $100 | const shield = new AIShield({ preset: "public_website" }); ## Cost Tracking Token counting and budget enforcement. Uses Redis for distributed tracking, falls back to in-memory. ### Budget Enforcement const shield = new AIShield({ cost: { enabled: true, budgets: { "chatbot": { softLimit: 5, hardLimit: 10, period: "daily" }, "global": { softLimit: 80, hardLimit: 100, period: "daily" }, }, }, }); // Pre-flight check const budget = await shield.checkBudget("chatbot", "gpt-4o", 1000, 500); // budget.allowed, budget.currentSpend, budget.remainingBudget, budget.warning // Record actual cost await shield.recordCost("chatbot", "gpt-4o", promptTokens, completionTokens); ### Budget Periods - `hourly` — resets every hour - `daily` — resets every day (UTC) - `monthly` — resets every month ### Redis Integration import Redis from "ioredis"; import { CostTracker } from "ai-shield-core"; const redis = new Redis(process.env.REDIS_URL); const tracker = new CostTracker(budgets, redis); ### Model Pricing Built-in pricing table (Jun 2026): | Model | Input/1M | Output/1M | |-------|----------|----------| | GPT-5.2 | $2.50 | $10.00 | | GPT-4o | $2.50 | $10.00 | | GPT-4o-mini | $0.15 | $0.60 | | o3 | $10.00 | $40.00 | | Claude Fable 5 | $10.00 | $50.00 | | Claude Opus 4.8 | $5.00 | $25.00 | | Claude Opus 4.7 | $5.00 | $25.00 | | Claude Sonnet 4.6 | $3.00 | $15.00 | | Claude Haiku 4.5 | $1.00 | $5.00 | ### Anomaly Detection Z-score based anomaly detection flags unusual spending (>2.5 standard deviations). import { detectAnomaly } from "ai-shield-core"; const result = detectAnomaly(currentDaySpend, historicalDailySpends); if (result.isAnomaly) { // Alert: unusual spending pattern // result.zScore, result.mean, result.stdDev } ## Canary Tokens Inject invisible markers into system prompts. If they appear in responses, prompt extraction is detected. import { injectCanary, checkCanaryLeak } from "ai-shield-core"; // Inject const { injectedPrompt, canaryToken } = injectCanary(systemPrompt); // Check response if (checkCanaryLeak(llmResponse, canaryToken)) { // System prompt was extracted! } ## Audit Logging Batched audit logging with pluggable backends. Stores metadata and hashes (not raw content) for GDPR/DSGVO compliance. Currently supports `console` and `memory` stores. PostgreSQL store is planned (see Roadmap). ### PostgreSQL Schema CREATE TABLE ai_shield_audit ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), session_id TEXT, agent_id TEXT, user_id_hash TEXT, request_type TEXT NOT NULL, -- 'chat' | 'tool_call' | 'agent_to_agent' input_hash TEXT NOT NULL, -- SHA-256, NOT the raw input model TEXT, security_decision TEXT NOT NULL, -- 'allow' | 'warn' | 'block' security_reason TEXT, violations JSONB DEFAULT '[]', scan_duration_ms REAL, cost_usd NUMERIC(10,6) ) PARTITION BY RANGE (timestamp); -- Monthly partitions for retention management -- Indexes on timestamp, agent_id, security_decision ### Configuration const shield = new AIShield({ audit: { enabled: true, store: "console", // "console" | "memory" (postgresql planned) batchSize: 100, // flush every 100 records flushIntervalMs: 1000, // or every 1 second }, }); ## Scan Result Every scan returns a `ScanResult`: interface ScanResult { safe: boolean; // true if decision is "allow" decision: "allow" | "warn" | "block"; sanitized: string; // input with PII masked violations: Violation[]; // what was found meta: { scanDurationMs: number; // total scan time scannersRun: string[]; // ["heuristic", "pii", "tool_policy"] cached: boolean; }; } interface Violation { type: "prompt_injection" | "pii_detected" | "tool_denied" | "manifest_drift" | ...; scanner: string; // which scanner flagged it score: number; // 0.0 - 1.0 threshold: number; // configured threshold message: string; // human-readable detail?: string; // technical detail } ## Error Handling The SDK wrapper packages throw typed errors: import { ShieldBlockError, ShieldBudgetError } from "ai-shield-openai"; try { const response = await shielded.createChatCompletion(params); } catch (err) { if (err instanceof ShieldBlockError) { // Input was blocked console.log(err.scanResult.violations); } if (err instanceof ShieldBudgetError) { // Budget exceeded console.log(err.budgetCheck.currentSpend); } } ## Project Structure ai-shield/ ├── packages/ │ ├── core/ ai-shield-core │ │ └── src/ │ │ ├── index.ts Public API + shield() one-liner │ │ ├── shield.ts AIShield main class │ │ ├── types.ts All shared types │ │ ├── scanner/ │ │ │ ├── chain.ts Scanner chain orchestrator │ │ │ ├── heuristic.ts Prompt injection detection (40+ patterns) │ │ │ ├── pii.ts PII detection (DE/EU-first) │ │ │ └── canary.ts Canary token injection │ │ ├── policy/ │ │ │ ├── engine.ts 3 presets (public/internal/ops) │ │ │ └── tools.ts MCP tool permissions + manifest pinning │ │ ├── cost/ │ │ │ ├── tracker.ts Budget enforcement (Redis/memory) │ │ │ ├── pricing.ts Model pricing table │ │ │ └── anomaly.ts Z-score anomaly detection │ │ └── audit/ │ │ ├── logger.ts Batched audit logging │ │ ├── types.ts AuditStore interface │ │ └── schema.sql PostgreSQL schema │ │ │ ├── openai/ ai-shield-openai │ │ └── src/ │ │ ├── index.ts createShield() factory │ │ └── wrapper.ts ShieldedOpenAI class │ │ │ ├── anthropic/ ai-shield-anthropic │ │ └── src/ │ │ ├── index.ts createShield() factory │ │ └── wrapper.ts ShieldedAnthropic class │ │ │ ├── gemini/ ai-shield-gemini │ │ └── src/ │ │ ├── index.ts createShield() factory │ │ └── wrapper.ts ShieldedGemini class │ │ │ └── middleware/ ai-shield-middleware │ └── src/ │ ├── index.ts Combined exports │ ├── shared.ts Shared scan logic │ ├── express.ts Express middleware │ └── hono.ts Hono middleware │ ├── tests/ │ └── unit/ │ ├── heuristic.test.ts 42 tests │ ├── cost.test.ts 26 tests │ ├── pii.test.ts 20 tests │ ├── policy-engine.test.ts 16 tests │ ├── chain.test.ts 15 tests │ ├── middleware.test.ts 13 tests │ ├── shield.test.ts 13 tests │ ├── audit.test.ts 13 tests │ ├── tools.test.ts 12 tests │ ├── openai-wrapper.test.ts 9 tests │ ├── canary.test.ts 7 tests │ ├── gemini-wrapper.test.ts 12 tests │ ├── gemini-stream.test.ts 5 tests │ └── anthropic-wrapper.test.ts 7 tests │ ├── package.json Monorepo root (npm workspaces) ├── tsconfig.json Strict TypeScript └── vitest.config.ts Test config ## Tests npm test # 672 tests, ~1s | Suite | Tests | Covers | |-------|------:|--------| | Heuristic | 42 | 23 injection prompts, 15 clean prompts, config, performance | | Cost | 26 | Budget checks, cost recording, pricing table, anomaly z-score | | LRU Cache | 20 | Get/set, LRU eviction, TTL expiry, prune, AIShield integration | | PII | 20 | IBAN, credit card, email, phone, tax ID, IP, URL, masking, modes | | PII Extended | 16 | Edge cases, overlap dedup, multi-type | | Policy Engine | 16 | All 3 presets, thresholds, PII actions, tool policies, budgets | | Heuristic Extended | 15 | Advanced patterns, structural signals, edge cases | | Scanner Chain | 15 | Execution, escalation, early-exit, sanitization, metadata | | Full Pipeline | 14 | End-to-end integration, preset combos | | Middleware | 13 | Input extraction (6 fields + messages[]), blocked response format | | Shield | 13 | Default config, presets, tool policy, cost, convenience, metadata | | Audit | 13 | Logging, SHA-256 hashing, batching, flush, close | | Gemini Wrapper | 12 | Clean input (string, array, params), injection blocking, PII masking, callbacks, output scan, tool context | | Tool Policy | 12 | Allow/deny, wildcards, manifest pin/drift, performance | | OpenAI Stream | 10 | Chunk accumulation, pre-stream blocking, cost recording, done/text props | | Middleware Express | 10 | Express integration, error handling, skip paths | | OpenAI Wrapper | 9 | Clean input, injection blocking, PII masking, callbacks, output scan | | Anthropic Stream | 9 | Chunk accumulation, pre-stream blocking, cost recording, output scan | | Middleware Hono | 8 | Hono integration, context injection | | Singleton | 8 | Instance management, config reuse | | Canary | 7 | Token injection, uniqueness, leak detection | | Anthropic Wrapper | 7 | Clean input, injection blocking, PII masking, multi-block, output scan | | Gemini Stream | 10 | Chunk accumulation, pre-stream blocking, output scan, shieldResult, response promise, done state, onBlocked callback, modelName config | ## Dependencies Minimal by design. Core has zero runtime dependencies. Optional peer deps for Redis and PostgreSQL. | Package | Required | Purpose | |---------|----------|--------| | `ioredis` | No | Distributed budget tracking | | `pg` | No | PostgreSQL audit logging | | `openai` | Peer dep of `ai-shield-openai` | OpenAI SDK wrapper | | `@anthropic-ai/sdk` | Peer dep of `ai-shield-anthropic` | Anthropic SDK wrapper | | `@google/generative-ai` | Peer dep of `ai-shield-gemini` | Gemini SDK wrapper | | `express` | Peer dep of `ai-shield-middleware` | Express middleware | | `hono` | Peer dep of `ai-shield-middleware` | Hono middleware | ## Roadmap ### Shipped in v0.5.0 (this release) - [x] **Typoglycemia defense** (`unscrambleForInjectionScan` + `damerauLevenshtein`) — un-scrambles "ignroe pevrious instrcutions" via anagram-middle or single-edit fuzzy match, first+last gate keeps benign prose clean - [x] **Dual-LLM privilege separation** (`createDualLLM`) — privileged (tools) model never reads raw untrusted content; quarantined (no-tools) model processes it, results scanned + fenced, unsafe results dropped - [x] **Action screening** (`createActionScreener`) — fail-closed gate checking a tool call against the user's original intent ### Shipped in v0.4.0 - [x] **Unicode TAG-smuggling detection** (U+E0000–E007F de-tagging + standalone-tag signal) - [x] **Multilingual override detection** (DE/ES/FR `localized_override` category) - [x] **Policy-puppetry / fake-config detection** (HiddenLayer 2025 — DELIM-PP-1..5) - [x] **Leetspeak detection** (`leetDecodeForInjectionScan` lossy view) - [x] **Attack-corpus harness** (100% detection / 0% false-positive bar) + 2 bug fixes + 3 FP fixes ### Shipped in v0.3.0 - [x] **Output scanning** (`scanOutput`) — OWASP LLM05: SQL/shell/XSS/template payloads, secret leaks, system-prompt leaks, output-side PII - [x] **Tool-output scanner** (`scanToolOutput`) — runtime tool-result indirect injection (`tool-output` source) - [x] **Multi-agent trust propagation** (`propagateTrust`) — sticky contagion tracking across agent chains - [x] **Async LLM-as-Judge** (`createAsyncJudge`) — semantic detection off the hot path, BYO-backend - [x] **Unicode / evasion hardening** — letter-splitting collapse, extended homoglyph map, adversarial-suffix signal ### Shipped in v0.2.0 - [x] LRU scan cache (TTL + LRU eviction) - [x] Streaming support (OpenAI + Anthropic + Gemini) - [x] Canary token detection (system-prompt extraction) - [x] **Indirect prompt injection scanner** (RAG / tool-desc / memory / web / agent-output) - [x] **Trust-tier context streams** (`wrapContext` / `assemblePrompt`) - [x] **Memory canary + persistence-poisoning detection** - [x] **Circuit breakers + HITL gate** for tool runtime guard - [x] **ONNX DeBERTa ML classifier** (optional `ai-shield-classifier-onnx` package) ### Next - [ ] `@google/genai` wrapper (new Gemini SDK, replacing `@google/generative-ai`) - [ ] Bloom filter for known-good/bad inputs - [ ] PostgreSQL audit store (`store: "postgresql"` currently falls back to console) - [ ] Toxicity / bias detection - [ ] Dashboard (Next.js) ## About StudioMeyer [StudioMeyer](https://studiomeyer.io) is an AI and design studio based in Palma de Mallorca, working with clients worldwide. We build custom websites and AI infrastructure for small and medium businesses. Production stack on Claude Agent SDK, MCP and n8n, with Sentry, Langfuse and LangGraph for observability and an in-house guard layer. ## License MIT
Built by [StudioMeyer](https://studiomeyer.io) [Darwin Agents](https://github.com/studiomeyer-io/darwin-agents) · [Agent Fleet](https://github.com/studiomeyer-io/agent-fleet) · [MCP Video](https://github.com/studiomeyer-io/mcp-video)
标签:TypeScript, 大语言模型安全, 安全插件, 提示注入防御, 数据脱敏, 机密管理, 源代码安全, 自动化攻击, 零依赖