-
Notifications
You must be signed in to change notification settings - Fork 109
feat(safety): add native PII detection and response validation to generate/stream #952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -224,6 +224,8 @@ | |
| import { isNonNullObject } from "./utils/typeUtils.js"; | ||
| import { getWorkflow } from "./workflow/core/workflowRegistry.js"; | ||
| import { runWorkflow } from "./workflow/core/workflowRunner.js"; | ||
| import { detectAndRedactPII } from "./utils/piiDetector.js"; | ||
| import { validateResponse } from "./utils/responseValidator.js"; | ||
|
|
||
| /** | ||
| * NL-002: Classify MCP error messages into categories for AI disambiguation. | ||
|
|
@@ -3179,7 +3181,7 @@ | |
| * Initialize event listeners that feed span data to MetricsAggregator. | ||
| * Listens to generation:end, stream:complete, and tool:end events. | ||
| */ | ||
| private initializeMetricsListeners(): void { | ||
|
Check warning on line 3184 in src/lib/neurolink.ts
|
||
| this.emitter.on("generation:end", ((...args: unknown[]) => { | ||
| const data = args[0] as Record<string, unknown>; | ||
| // A2 fix: When Pipeline A (AI SDK → @langfuse/otel) already creates a | ||
|
|
@@ -3896,6 +3898,49 @@ | |
| options.input?.text, | ||
| "Input text is required and must be a non-empty string", | ||
| ); | ||
|
|
||
| // Input validation (trimWhitespace, minLength, maxLength, requireContent) | ||
| if (options.inputValidation) { | ||
| const iv = options.inputValidation; | ||
| if (iv.trimWhitespace) { | ||
| options.input.text = options.input.text.trim(); | ||
| } | ||
| if (iv.requireContent && !options.input.text.trim()) { | ||
| throw new Error( | ||
| "Input content is required but was empty or whitespace", | ||
| ); | ||
| } | ||
| if (iv.minLength && options.input.text.length < iv.minLength) { | ||
| throw new Error( | ||
| `Input text is too short (${options.input.text.length} < ${iv.minLength})`, | ||
| ); | ||
| } | ||
| if (iv.maxLength && options.input.text.length > iv.maxLength) { | ||
| throw new Error( | ||
| `Input text is too long (${options.input.text.length} > ${iv.maxLength})`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // PII detection and redaction | ||
| if (options.piiDetection?.enabled) { | ||
| const piiResult = await detectAndRedactPII(options.input.text, { | ||
| enabled: true, | ||
| action: options.piiDetection.action ?? "warn", | ||
| detectTypes: options.piiDetection.detectTypes, | ||
| customPatterns: options.piiDetection.customPatterns, | ||
| allowList: options.piiDetection.allowList, | ||
| redactionText: options.piiDetection.redactionText, | ||
| }); | ||
| if (piiResult.action === "abort") { | ||
| throw new Error( | ||
| piiResult.feedback ?? "Request blocked: PII detected in input", | ||
| ); | ||
|
Comment on lines
+3908
to
+3938
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return typed validation errors for the new guardrails. These new validation/PII/response-validation failures currently throw bare Also applies to: 4002-4006, 6419-6449 🤖 Prompt for AI Agents |
||
| } | ||
| // Replace input text with redacted version | ||
| options.input.text = piiResult.text; | ||
| } | ||
|
|
||
| this.enforceSessionBudget(options.maxBudgetUsd); | ||
| this.applyGenerateLifecycleMiddleware(options); | ||
| await this.applyAuthenticatedRequestContext(options); | ||
|
|
@@ -4229,6 +4274,34 @@ | |
| ...(textResult.retries && { retries: textResult.retries }), | ||
| }; | ||
|
|
||
| // Response validation (if configured) | ||
| if (options.responseValidation) { | ||
| const validationResult = validateResponse(generateResult.content ?? "", { | ||
| minLength: options.responseValidation.minLength, | ||
| maxLength: options.responseValidation.maxLength, | ||
| requiredPhrases: options.responseValidation.requiredPhrases, | ||
| forbiddenPhrases: options.responseValidation.forbiddenPhrases, | ||
| jsonSchema: options.responseValidation.jsonSchema, | ||
| customValidator: options.responseValidation.customValidator, | ||
| truncationAction: options.responseValidation.truncationAction, | ||
| truncationSuffix: options.responseValidation.truncationSuffix, | ||
| retryOnFailure: options.responseValidation.retryOnFailure, | ||
| maxRetries: options.responseValidation.maxRetries, | ||
| }); | ||
|
|
||
| if (validationResult.action === "abort") { | ||
| throw new Error( | ||
| validationResult.feedback ?? | ||
| `Response validation failed: ${validationResult.issues.map((i) => i.message).join("; ")}`, | ||
| ); | ||
| } | ||
|
|
||
| // Apply validated/truncated text | ||
| if (validationResult.text !== generateResult.content) { | ||
| generateResult.content = validationResult.text; | ||
| } | ||
| } | ||
|
|
||
| if (generateResult.analytics?.cost && generateResult.analytics.cost > 0) { | ||
| this._sessionCostUsd += generateResult.analytics.cost; | ||
| } | ||
|
|
@@ -6138,7 +6211,7 @@ | |
| /** | ||
| * Direct provider generation (no MCP, no recursion) | ||
| */ | ||
| private async directProviderGeneration( | ||
|
Check warning on line 6214 in src/lib/neurolink.ts
|
||
| options: TextGenerationOptions, | ||
| ): Promise<TextGenerationResult> { | ||
| const startTime = Date.now(); | ||
|
|
@@ -6999,6 +7072,48 @@ | |
| startTime: number, | ||
| ): Promise<void> { | ||
| await this.validateStreamInput(options); | ||
|
|
||
| // Input validation for stream | ||
| if (options.inputValidation && options.input?.text) { | ||
| const iv = options.inputValidation; | ||
| if (iv.trimWhitespace) { | ||
| options.input.text = options.input.text.trim(); | ||
| } | ||
| if (iv.requireContent && !options.input.text.trim()) { | ||
| throw new Error( | ||
| "Input content is required but was empty or whitespace", | ||
| ); | ||
| } | ||
| if (iv.minLength && options.input.text.length < iv.minLength) { | ||
| throw new Error( | ||
| `Input text is too short (${options.input.text.length} < ${iv.minLength})`, | ||
| ); | ||
| } | ||
| if (iv.maxLength && options.input.text.length > iv.maxLength) { | ||
| throw new Error( | ||
| `Input text is too long (${options.input.text.length} > ${iv.maxLength})`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // PII detection and redaction for stream | ||
| if (options.piiDetection?.enabled && options.input?.text) { | ||
| const piiResult = await detectAndRedactPII(options.input.text, { | ||
| enabled: true, | ||
| action: options.piiDetection.action ?? "warn", | ||
| detectTypes: options.piiDetection.detectTypes, | ||
| customPatterns: options.piiDetection.customPatterns, | ||
| allowList: options.piiDetection.allowList, | ||
| redactionText: options.piiDetection.redactionText, | ||
| }); | ||
| if (piiResult.action === "abort") { | ||
| throw new Error( | ||
| piiResult.feedback ?? "Request blocked: PII detected in input", | ||
| ); | ||
| } | ||
| options.input.text = piiResult.text; | ||
| } | ||
|
Comment on lines
+7076
to
+7115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apply output response validation in the streaming path too. The stream path now has input-side safety, but streamed output never calls For streaming, enforce Also applies to: 6584-6661 🤖 Prompt for AI Agents |
||
|
|
||
| this.enforceSessionBudget(options.maxBudgetUsd); | ||
| await this.applyAuthenticatedRequestContext(options); | ||
| this.emitStreamStartEvents(options, startTime); | ||
|
|
@@ -7046,7 +7161,7 @@ | |
| return result; | ||
| } | ||
|
|
||
| private async runStandardStreamRequest(params: { | ||
|
Check warning on line 7164 in src/lib/neurolink.ts
|
||
| options: StreamOptions; | ||
| streamSpan: ReturnType<typeof tracers.sdk.startSpan>; | ||
| spanStartTime: number; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid exposing safety flags on commands that silently ignore them.
Line 487 adds these flags to
commonOptions, sobatch,provider status,setup,loop, and other commands accept them too. OnlyexecuteGenerate()andexecuteRealStream()forward them; for example,neurolink batch prompts.txt --pii-redactis accepted butexecuteBatch()does not passpiiDetection, so prompts can be sent unredacted. Move these flags into generate/stream-specific options, or wire them through every command that exposes them.Suggested direction
Then pass
CLICommandFactory.safetyOptionsasadditionalOptionsonly fromcreateGenerateCommand()andcreateStreamCommand()unless batch support is intentionally implemented too.🤖 Prompt for AI Agents