@directive-run/ai 1.19.0 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/chunk-2C3R77ZW.cjs +16 -0
- package/dist/chunk-2C3R77ZW.cjs.map +1 -0
- package/dist/chunk-ARYO7BLK.js +16 -0
- package/dist/chunk-ARYO7BLK.js.map +1 -0
- package/dist/chunk-MPAX6C5V.cjs +4 -0
- package/dist/chunk-MPAX6C5V.cjs.map +1 -0
- package/dist/chunk-WERHNNUD.js +4 -0
- package/dist/chunk-WERHNNUD.js.map +1 -0
- package/dist/{debug-timeline-Dn1lC_gK.d.cts → debug-timeline-DpnRMnLU.d.cts} +1 -1
- package/dist/{debug-timeline-BGcPa0JO.d.ts → debug-timeline-L13P-U2I.d.ts} +1 -1
- package/dist/devtools.d.cts +5 -5
- package/dist/devtools.d.ts +5 -5
- package/dist/evals.d.cts +2 -2
- package/dist/evals.d.ts +2 -2
- package/dist/gemini.d.cts +1 -1
- package/dist/gemini.d.ts +1 -1
- package/dist/guardrails.cjs +1 -1
- package/dist/guardrails.cjs.map +1 -1
- package/dist/guardrails.d.cts +616 -4
- package/dist/guardrails.d.ts +616 -4
- package/dist/guardrails.js +1 -1
- package/dist/guardrails.js.map +1 -1
- package/dist/{health-monitor-Cq5sDvid.d.cts → health-monitor-C6xoXrQz.d.cts} +1 -1
- package/dist/{health-monitor-DtwmIJkb.d.ts → health-monitor-qL9RNMH3.d.ts} +1 -1
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +11 -11
- package/dist/index.js.map +1 -1
- package/dist/multi-agent.d.cts +6 -6
- package/dist/multi-agent.d.ts +6 -6
- package/dist/ollama.d.cts +1 -1
- package/dist/ollama.d.ts +1 -1
- package/dist/openai.d.cts +2 -2
- package/dist/openai.d.ts +2 -2
- package/dist/{orchestrator-types-s0766dDB.d.cts → orchestrator-types-C-sgbBQX.d.cts} +20 -3
- package/dist/{orchestrator-types-C5D5JZUj.d.ts → orchestrator-types-CX1PqpKR.d.ts} +20 -3
- package/dist/predicate.d.cts +1 -1
- package/dist/predicate.d.ts +1 -1
- package/dist/{semantic-cache-Ba0-jEwb.d.cts → semantic-cache-DM7ev7NQ.d.cts} +1 -1
- package/dist/{semantic-cache-Ba0-jEwb.d.ts → semantic-cache-DM7ev7NQ.d.ts} +1 -1
- package/dist/testing.cjs +1 -1
- package/dist/testing.d.cts +4 -4
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +1 -1
- package/dist/{types-CUp1N0uU.d.cts → types-DJ09LjZX.d.cts} +1 -1
- package/dist/{types-CUp1N0uU.d.ts → types-DJ09LjZX.d.ts} +1 -1
- package/package.json +2 -2
- package/dist/ann-index-27QV9tPC.d.ts +0 -616
- package/dist/ann-index-HoZFIUA-.d.cts +0 -616
- package/dist/chunk-2PG26JZU.cjs +0 -16
- package/dist/chunk-2PG26JZU.cjs.map +0 -1
- package/dist/chunk-BFSRYCOS.js +0 -4
- package/dist/chunk-BFSRYCOS.js.map +0 -1
- package/dist/chunk-CAA6Q4YY.cjs +0 -4
- package/dist/chunk-CAA6Q4YY.cjs.map +0 -1
- package/dist/chunk-MIHH5GPT.js +0 -16
- package/dist/chunk-MIHH5GPT.js.map +0 -1
package/dist/guardrails.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/guardrails/fact-pii.ts"],"names":["luhnValid","value","digits","sum","isEven","i","ch","d","SYNC_PATTERNS","scanText","text","types","out","pattern","match","redactText","matches","sorted","a","b","tokenByType","m","createFactPIIGuardrail","options","mode","includeKeys","excludeKeys","onBlocked","customDetector","walkDepth","typeSet","screenedKeys","excludedSet","effectiveWalkDepth","initialized","systemRef","initScreenedKeys","system","piiTagged","entry","runScan","all","inspect","depth","detected","mutatedArr","nested","mutated","k","v","key","_prev","result","facts","changes","change"],"mappings":"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","file":"guardrails.cjs","sourcesContent":["/**\n * Fact-PII Guardrail — input guardrail at the fact-store boundary\n *\n * Closes the source → fact → agent prompt PII bypass surfaced by R5's\n * red-team / privacy / AI-integration reviewers: `createPIIGuardrail`\n * only inspects the `data.input` string at runStream entry, so PII that\n * a source publishes into a fact (a Supabase realtime row carrying a\n * customer email, a webhook payload with a SSN, an MCP server's\n * resource notification with a card number) reaches the agent's\n * prompt — via fact injection — without ever hitting the input\n * guardrail chain.\n *\n * This plugin runs as a Directive plugin (`onFactSet` / `onFactsBatch`),\n * scans every write to a pii-tagged fact against a sync regex matcher\n * for the three highest-volume PII categories (SSN, credit card, email),\n * and either **redacts** the value (the default — safe shipping posture)\n * or **rejects** the write (throws so the source's publish handler can\n * surface the violation). Operators wire it once at `createSystem`; no\n * per-source / per-fact changes are required.\n *\n * The async PII detector from `pii-enhanced.ts` is unsuitable here:\n * `onFactSet` is synchronous and a deferred detection would let the\n * raw PII reach observers + breakpoints + audit-ledger before the\n * redaction completed. Built-in matching is therefore inlined as\n * synchronous regex. Consumers who need richer detection pass a\n * synchronous `customDetector`.\n *\n * @example Defensive (redact PII writes into pii-tagged facts)\n * ```ts\n * import { createSystem, t } from '@directive-run/core';\n * import { createFactPIIGuardrail } from '@directive-run/ai/guardrails';\n *\n * const customer = createModule('customer', {\n * schema: {\n * facts: {\n * email: t.string().meta({ tags: ['pii'] }),\n * ssn: t.string().meta({ tags: ['pii'] }),\n * },\n * },\n * sources: {\n * supabase: { attach: (publish) => subscribe(publish) },\n * },\n * });\n *\n * const system = createSystem({\n * module: customer,\n * plugins: [\n * createFactPIIGuardrail({\n * mode: 'redact',\n * onBlocked: (key, detected) => {\n * console.warn(`[fact-pii] redacted ${detected.length} match(es) in ${key}`);\n * },\n * }),\n * ],\n * });\n * ```\n *\n * @example Monitor-only (alert on every PII match; don't mutate the fact)\n * ```ts\n * createFactPIIGuardrail({\n * mode: 'alert',\n * onBlocked: (key) => Sentry.captureException(new Error(`pii match: ${key}`)),\n * });\n * ```\n *\n * @example Allow specific keys (not just by tag)\n * ```ts\n * createFactPIIGuardrail({\n * includeKeys: ['customer.email', 'customer.phone'],\n * });\n * ```\n */\n\nimport type { Plugin, System } from \"@directive-run/core\";\n\n// ============================================================================\n// Sync PII patterns\n// ============================================================================\n\n/**\n * Public match record for a single PII finding. Mirrors `DetectedPII` from\n * `pii-enhanced.ts` so a downstream guardrail can normalize against either\n * detection path.\n */\nexport interface FactPIIMatch {\n type: FactPIICategory;\n value: string;\n start: number;\n end: number;\n}\n\n/** PII categories the built-in synchronous detector covers. */\nexport type FactPIICategory = \"ssn\" | \"credit_card\" | \"email\";\n\ninterface SyncPattern {\n type: FactPIICategory;\n regex: RegExp;\n /**\n * Optional post-regex validator. Returning false drops the match.\n * Used by credit-card detection to apply the Luhn algorithm (mirrors\n * the validator in `pii-enhanced.ts`) so phone numbers / tracking IDs\n * formatted with separators don't mass-redact as credit cards.\n */\n validate?: (value: string) => boolean;\n redactionToken: string;\n}\n\n/**\n * Luhn checksum — drops credit-card false positives the regex would\n * otherwise pull in (any 13-19 digit sequence with separators). Mirrors\n * the validator in `pii-enhanced.ts` so the two detection paths agree.\n */\nfunction luhnValid(value: string): boolean {\n const digits = value.replace(/[\\s-]/g, \"\");\n if (digits.length < 13 || digits.length > 19) return false;\n if (!/^\\d+$/.test(digits)) return false;\n let sum = 0;\n let isEven = false;\n for (let i = digits.length - 1; i >= 0; i--) {\n const ch = digits[i];\n if (!ch) continue;\n let d = Number.parseInt(ch, 10);\n if (isEven) {\n d *= 2;\n if (d > 9) d -= 9;\n }\n sum += d;\n isEven = !isEven;\n }\n return sum % 10 === 0;\n}\n\n// Synchronous patterns. These mirror the highest-volume categories from\n// pii-enhanced.ts; richer detection (addresses, names, phones) is the\n// caller's responsibility via `customDetector` because they require\n// context-aware logic that's a poor fit for a per-fact-write hook.\n//\n// SSN pattern accepts the canonical XXX-XX-XXXX format. Internal IDs\n// formatted the same way will false-positive; the trade-off favors\n// safety (the redaction token is harmless if applied to a non-SSN).\n//\n// Credit-card pattern combines the broad 13-19-digit regex with a Luhn\n// validator so phone numbers, tracking IDs, and other long digit\n// sequences with separators are NOT swept up. This mirrors how\n// `pii-enhanced.ts` keeps its credit-card detection accurate.\nconst SYNC_PATTERNS: SyncPattern[] = [\n {\n type: \"ssn\",\n regex: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,\n redactionToken: \"[SSN]\",\n },\n {\n type: \"credit_card\",\n regex: /\\b((?:\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4})|\\d{13,19})\\b/g,\n validate: luhnValid,\n redactionToken: \"[CREDIT_CARD]\",\n },\n {\n type: \"email\",\n regex: /\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}\\b/gi,\n redactionToken: \"[EMAIL]\",\n },\n];\n\nfunction scanText(\n text: string,\n types: ReadonlySet<FactPIICategory>,\n): FactPIIMatch[] {\n const out: FactPIIMatch[] = [];\n for (const pattern of SYNC_PATTERNS) {\n if (!types.has(pattern.type)) continue;\n pattern.regex.lastIndex = 0;\n let match: RegExpExecArray | null;\n // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic exec loop\n while ((match = pattern.regex.exec(text)) !== null) {\n if (pattern.validate && !pattern.validate(match[0])) continue;\n out.push({\n type: pattern.type,\n value: match[0],\n start: match.index,\n end: match.index + match[0].length,\n });\n }\n }\n return out;\n}\n\nfunction redactText(text: string, matches: FactPIIMatch[]): string {\n if (matches.length === 0) return text;\n // Sort matches by start position descending so splicing doesn't shift\n // subsequent offsets.\n const sorted = [...matches].sort((a, b) => b.start - a.start);\n const tokenByType: Record<FactPIICategory, string> = {\n ssn: \"[SSN]\",\n credit_card: \"[CREDIT_CARD]\",\n email: \"[EMAIL]\",\n };\n let out = text;\n for (const m of sorted) {\n out = out.slice(0, m.start) + tokenByType[m.type] + out.slice(m.end);\n }\n return out;\n}\n\n// ============================================================================\n// Options\n// ============================================================================\n\n/**\n * Behavior when a pii-tagged fact's incoming value contains detected PII.\n *\n * - `\"redact\"` (default): the fact is rewritten with redacted text (e.g.\n * `\"[SSN]\"`) via a follow-up store write. The system briefly observes\n * the raw value during the same microtask the publish landed in, then\n * the redacted value overwrites it before any reconcile / agent runs.\n * Safe shipping posture for production: the LLM call always sees the\n * redacted value.\n * - `\"alert\"`: fire `onBlocked` but DO NOT mutate the fact. The raw value\n * stays in the store. Use this for monitoring-only deployments where\n * the source's transport is already trusted but a regression detector\n * is needed (paging ops on every match).\n *\n * Note: Directive plugin hooks (`onFactSet`, `onFactsBatch`) are\n * wrapped by the plugin manager's `safeCall` so a throw from inside the\n * hook is swallowed. The guardrail therefore cannot reject the write\n * itself — it can only observe + redact-via-followup-write or alert.\n * For hard rejection at the publish boundary, a future RFC will add a\n * pre-commit transform hook on the source primitive.\n */\nexport type FactPIIGuardrailMode = \"redact\" | \"alert\";\n\nexport interface FactPIIGuardrailOptions {\n /** Default: `\"redact\"` */\n mode?: FactPIIGuardrailMode;\n /**\n * Built-in categories to scan for. Default: all three (`ssn`,\n * `credit_card`, `email`). Pass `[]` to opt out of the built-ins and\n * rely entirely on `customDetector`.\n */\n types?: readonly FactPIICategory[];\n /**\n * Specific fact keys to scan in addition to the auto-detected\n * `pii`-tagged set. Useful when a consumer can't change the schema's\n * meta but knows the key should be screened.\n */\n includeKeys?: readonly string[];\n /**\n * Exclude these fact keys even if they're pii-tagged. Escape hatch for\n * a key that's already pre-sanitized upstream of the manager.\n */\n excludeKeys?: readonly string[];\n /**\n * Called whenever the guardrail detects PII and acts on it. Receives the\n * fact key, the detected matches, and the action that was taken. Fires\n * AFTER redact + before any throw in `reject` mode. Use this to alert\n * SREs without coupling the guardrail to a specific logging backend.\n */\n onBlocked?: (\n key: string,\n detected: readonly FactPIIMatch[],\n action: FactPIIGuardrailMode,\n ) => void;\n /**\n * Custom synchronous detector that runs alongside the built-in regex\n * scanner. The union of detections is acted on. Useful when the\n * consumer ships a domain-specific PII detector (e.g. internal\n * account-number format). MUST be synchronous — `onFactSet` cannot\n * await deferred work.\n */\n customDetector?: (text: string) => readonly FactPIIMatch[];\n /**\n * Maximum nesting depth to walk when scanning a structured fact\n * value. Default `1` — the scanner inspects top-level strings, then\n * descends one level into the object/array. Arrays and plain objects\n * count as one depth level each. Maps and Sets are still NOT walked\n * by the built-in scanner; PII inside those structures must go\n * through a `customDetector`.\n *\n * Per R13-C6 the walker recurses into arrays (Supabase realtime\n * payloads ship as `payload.new = [{...}]`, MCP resource lists ship\n * as arrays — those shapes silently bypassed the Tier 0 guard before).\n *\n * Consumers with deeply-nested PII shapes have two options:\n * 1. Pass `walkDepth: 2` (or higher) — the scanner walks plain\n * objects and arrays to that depth. Maps and Sets are still\n * skipped.\n * 2. Pass a `customDetector` that walks the consumer-specific shape\n * and returns concrete matches — the right answer for\n * domain-specific structures (Maps, Sets, getters, Symbol keys).\n *\n * Maximum is `5` to prevent pathological recursion on cyclic\n * structures. Passing anything higher clamps to `5`.\n *\n * Property iteration uses `Object.entries`, which skips\n * Symbol-keyed properties and non-enumerable string keys. If you\n * store PII under a Symbol key (unusual), a `customDetector` is the\n * right escape hatch.\n */\n walkDepth?: number;\n}\n\n// ============================================================================\n// Plugin factory\n// ============================================================================\n\ninterface MetaCapableSystem {\n meta?: {\n byTag?: (tag: string) => Array<{ type?: string; id?: string }>;\n };\n facts?: {\n $store?: {\n set?: (k: string, v: unknown) => void;\n };\n };\n}\n\n/**\n * Create a Directive plugin that scans pii-tagged fact writes for PII and\n * redacts or rejects them at the manager boundary.\n *\n * Wire it once at `createSystem({ plugins: [...] })`. The plugin caches\n * the pii-tagged key set on `onInit` so per-write hooks are O(1) lookups.\n *\n * @returns a `Plugin` instance ready to add to `SystemConfig.plugins`.\n */\nexport function createFactPIIGuardrail(\n options: FactPIIGuardrailOptions = {},\n): Plugin {\n const {\n mode = \"redact\",\n types = [\"ssn\", \"credit_card\", \"email\"] as const,\n includeKeys = [],\n excludeKeys = [],\n onBlocked,\n customDetector,\n walkDepth = 1,\n } = options;\n\n const typeSet = new Set<FactPIICategory>(types);\n const screenedKeys = new Set<string>(includeKeys);\n const excludedSet = new Set(excludeKeys);\n // Clamp walkDepth to [1, 5]. Lower bound prevents accidental no-op\n // scans (`walkDepth: 0` would skip even top-level string members);\n // upper bound caps pathological recursion on cyclic structures.\n const effectiveWalkDepth = Math.max(1, Math.min(5, Math.floor(walkDepth)));\n let initialized = false;\n let systemRef: System | null = null;\n\n function initScreenedKeys(system: System): void {\n if (initialized) return;\n initialized = true;\n systemRef = system;\n const piiTagged = (system as unknown as MetaCapableSystem).meta?.byTag?.(\n \"pii\",\n );\n if (!piiTagged) return;\n for (const entry of piiTagged) {\n // MetaMatch shape: { type: \"fact\" | \"module\" | \"event\" | ..., id: string, meta }.\n // We screen FACT-typed matches only — agent prompts read facts, not\n // event / constraint / derivation metadata.\n if (entry.type !== \"fact\") continue;\n if (entry.id && !excludedSet.has(entry.id)) {\n screenedKeys.add(entry.id);\n }\n }\n }\n\n function runScan(text: string): FactPIIMatch[] {\n const all: FactPIIMatch[] = [];\n if (typeSet.size > 0) {\n for (const m of scanText(text, typeSet)) all.push(m);\n }\n if (customDetector) {\n for (const m of customDetector(text)) all.push(m);\n }\n return all;\n }\n\n function inspect(\n value: unknown,\n depth: number = effectiveWalkDepth,\n ):\n | { matched: false }\n | { matched: true; redacted: unknown; detected: FactPIIMatch[] } {\n if (typeof value === \"string\") {\n const detected = runScan(value);\n if (detected.length === 0) return { matched: false };\n return { matched: true, redacted: redactText(value, detected), detected };\n }\n if (depth <= 0) return { matched: false };\n if (Array.isArray(value)) {\n // Walk array elements at the same depth budget — R13 found that\n // real Supabase realtime payloads ship as `payload.new = [{ ... }]`\n // and MCP resource notifications ship as arrays of resources, so\n // skipping arrays silently no-ops the Tier 0 guard on the\n // dominant production ingest shape. Each element is inspected\n // independently; the array is rebuilt only if at least one\n // element matched. Symbol keys, Maps, and Sets are still out of\n // scope (caller must wire a `customDetector` for those).\n let mutatedArr: unknown[] | null = null;\n const all: FactPIIMatch[] = [];\n for (let i = 0; i < value.length; i++) {\n const nested = inspect(value[i], depth);\n if (!nested.matched) continue;\n if (mutatedArr === null) mutatedArr = [...value];\n mutatedArr[i] = nested.redacted;\n for (const d of nested.detected) all.push(d);\n }\n if (mutatedArr === null) return { matched: false };\n return { matched: true, redacted: mutatedArr, detected: all };\n }\n if (value && typeof value === \"object\") {\n // Walk plain objects up to `walkDepth` levels deep so a source\n // publishing a nested PII shape (e.g. `{ profile: { email } }`) is\n // screened when the consumer opts into `walkDepth: 2+`. Maps and\n // Sets are out of scope — pass a `customDetector` for those.\n // Symbol-keyed properties are NOT walked because `Object.entries`\n // skips them (documented in the JSDoc at the top of this file).\n let mutated: Record<string, unknown> | null = null;\n const all: FactPIIMatch[] = [];\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (typeof v === \"string\") {\n const detected = runScan(v);\n if (detected.length === 0) continue;\n if (mutated === null) {\n mutated = { ...(value as Record<string, unknown>) };\n }\n mutated[k] = redactText(v, detected);\n for (const d of detected) all.push(d);\n } else if (depth > 1 && v && typeof v === \"object\") {\n // Includes arrays — recurse and let the array branch above\n // handle the structural walk. Burns one depth level whether\n // the child is an array or another plain object so the\n // configured walkDepth bounds total recursion.\n const nested = inspect(v, depth - 1);\n if (!nested.matched) continue;\n if (mutated === null) {\n mutated = { ...(value as Record<string, unknown>) };\n }\n mutated[k] = nested.redacted;\n for (const d of nested.detected) all.push(d);\n }\n }\n if (mutated === null) return { matched: false };\n return { matched: true, redacted: mutated, detected: all };\n }\n return { matched: false };\n }\n\n return {\n name: \"fact-pii-guardrail\",\n\n onInit(system) {\n initScreenedKeys(system);\n },\n\n onFactSet(key, value, _prev) {\n if (!initialized) return;\n if (!screenedKeys.has(key)) return;\n const result = inspect(value);\n if (!result.matched) return;\n onBlocked?.(key, result.detected, mode);\n if (mode === \"alert\") return;\n // Redact mode: schedule a follow-up store write. `onFactSet` fires\n // post-commit, so the raw value briefly exists in the store; the\n // follow-up write overwrites it before the next reconcile / agent\n // read. Subscribers that snapshot the raw value during the same\n // microtask see it; the LLM call after the next settle does not.\n const facts = (systemRef as unknown as MetaCapableSystem | null)?.facts;\n if (facts?.$store?.set) {\n try {\n facts.$store.set(key, result.redacted);\n } catch {\n // Store rejected (e.g. unknown key, mid-destroy). onBlocked\n // already fired so the consumer has the signal.\n }\n }\n },\n\n onFactsBatch(changes) {\n if (!initialized) return;\n for (const change of changes) {\n if (change.type !== \"set\") continue;\n if (!screenedKeys.has(change.key)) continue;\n const result = inspect(change.value);\n if (!result.matched) continue;\n onBlocked?.(change.key, result.detected, mode);\n if (mode === \"alert\") continue;\n // Best-effort in-place mutation of the change record so post-batch\n // subscribers reading `change.value` see the redacted value\n // immediately. Frozen change records fall through silently.\n try {\n (change as { value: unknown }).value = result.redacted;\n } catch {\n // Frozen change record — fall through.\n }\n // Follow-up store write so the next read of `system.facts.<key>`\n // returns the redacted value too. Required because the batch\n // already committed the raw value at this point.\n const facts = (systemRef as unknown as MetaCapableSystem | null)?.facts;\n if (facts?.$store?.set) {\n try {\n facts.$store.set(change.key, result.redacted);\n } catch {\n /* store may reject mid-destroy; onBlocked already fired */\n }\n }\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"guardrails.cjs"}
|