promptPreprocessor.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.preprocess = preprocess;
const config_1 = require("./config");
const piiDetector_1 = require("./piiDetector");
const redactor_1 = require("./redactor");
const types_1 = require("./types");
// ---------------------------------------------------------------------------
// Pending confirmation state
// ---------------------------------------------------------------------------
/**
* Stores the redacted prompt while waiting for the user to confirm
* with --c. This ensures that on confirmation we forward the
* *redacted* text β never the original.
*/
let pendingRedactedPrompt = null;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Attempt one round of LLM + regex detection and produce a validated
* RedactionResult. Returns null if the result fails validation.
*/
async function attemptDetection(ctl, text) {
const pluginConfig = ctl.getPluginConfig(config_1.configSchematics);
const modelId = pluginConfig.get("piiModel").trim();
// Resolve the model: use the configured identifier or fall back to the
// currently loaded model.
// Note: the no-argument overload of model() does not accept options, so
// abort signal handling is delegated to the llmDetect call itself.
const model = modelId.length > 0
? await ctl.client.llm.model(modelId, { signal: ctl.abortSignal })
: await ctl.client.llm.model();
// LLM detection (structured output via Zod β throws on schema violation)
const llmResponse = await (0, piiDetector_1.llmDetect)(model, text, ctl.abortSignal);
// Regex fallback β adds any EMAIL / PHONE the LLM missed
const regexSpans = (0, piiDetector_1.regexDetect)(text);
// Merge: LLM entities first, then any regex-only additions
const merged = (0, piiDetector_1.mergeDetections)(llmResponse.entities, regexSpans);
// Assign identifiers and perform text substitution
const result = (0, redactor_1.redact)(text, merged);
// Validate the final structure and placeholder integrity
const parseResult = types_1.redactionResultSchema.safeParse(result);
if (!parseResult.success) {
ctl.debug("RedactionResult schema validation failed", parseResult.error.format());
return null;
}
if (!(0, redactor_1.validatePlaceholders)(result)) {
ctl.debug("Placeholder integrity check failed β some placeholders are missing from redacted text");
return null;
}
return result;
}
/**
* Format a human-readable summary of detected entities for the status panel.
*/
function formatEntitySummary(result) {
if (result.entities.length === 0) {
return "No PII detected.";
}
const lines = result.entities.map(e => ` β’ [[[${e.identifier}]]] β "${e.original}" (${e.type})`);
return `Detected ${result.entities.length} PII entity(ies):\n${lines.join("\n")}`;
}
// ---------------------------------------------------------------------------
// Main preprocessor
// ---------------------------------------------------------------------------
async function preprocess(ctl, userMessage) {
const rawText = userMessage.getText();
// ------------------------------------------------------------------
// Confirmation gate: if the user sent --c, forward the *redacted*
// text β never the original.
// ------------------------------------------------------------------
if (rawText === types_1.CONFIRM_SUFFIX) {
if (pendingRedactedPrompt === null) {
ctl.createStatus({
status: "error",
text: "No pending redaction to confirm. Please send your prompt first.",
});
throw new Error("No pending redaction to confirm. Please send your prompt first.");
}
const redactedText = pendingRedactedPrompt;
// Clear pending state now that confirmation has been consumed.
pendingRedactedPrompt = null;
const confirmStatus = ctl.createStatus({
status: "done",
text: "PII redaction confirmed β forwarding redacted prompt.",
});
ctl.debug("User confirmed redacted prompt. Forwarding saved redacted text.");
// Suppress unused variable warning β status is shown in the UI
void confirmStatus;
return redactedText;
}
const pluginConfig = ctl.getPluginConfig(config_1.configSchematics);
const requireConfirmation = pluginConfig.get("requireConfirmation");
const fallbackBehavior = pluginConfig.get("fallbackBehavior");
// ------------------------------------------------------------------
// Detection status
// ------------------------------------------------------------------
const detectionStatus = ctl.createStatus({
status: "loading",
text: "Detecting PII in promptβ¦",
});
let result = null;
// Attempt 1
try {
result = await attemptDetection(ctl, rawText);
}
catch (err) {
ctl.debug("PII detection attempt 1 failed", err);
}
// Attempt 2 (retry once on failure)
if (result === null) {
detectionStatus.setState({
status: "loading",
text: "PII detection failed β retryingβ¦",
});
try {
result = await attemptDetection(ctl, rawText);
}
catch (err) {
ctl.debug("PII detection attempt 2 failed", err);
}
}
// ------------------------------------------------------------------
// Fallback handling
// ------------------------------------------------------------------
if (result === null) {
if (fallbackBehavior === "passthrough") {
detectionStatus.setState({
status: "canceled",
text: "PII detection failed after retry β forwarding original prompt (passthrough mode).",
});
return userMessage;
}
else {
detectionStatus.setState({
status: "error",
text: "PII detection failed after retry. Request aborted. " +
"Check that a model is loaded and try again.",
});
throw new Error("PII detection failed after one retry. Request aborted per plugin configuration.");
}
}
// ------------------------------------------------------------------
// Success path
// ------------------------------------------------------------------
const entitySummary = formatEntitySummary(result);
if (result.entities.length === 0) {
// No PII found β pass through without modification
detectionStatus.setState({
status: "done",
text: "No PII detected β forwarding original prompt.",
});
return userMessage;
}
// ------------------------------------------------------------------
// Confirmation gate
// ------------------------------------------------------------------
if (requireConfirmation) {
// Save the redacted prompt so the --c handler can forward
// the redacted version (never the original).
pendingRedactedPrompt = result.redacted_text;
// Truncate the redacted text preview to avoid flooding the status panel
const preview = result.redacted_text.length > 500
? result.redacted_text.slice(0, 500) + "β¦"
: result.redacted_text;
detectionStatus.setState({
status: "canceled",
text: `PII detected β redaction preview shown below. ` +
`Send another message containing '--c' to confirm and proceed.\n\n` +
`--- Redacted prompt ---\n${preview}\n\n` +
`--- Entity map ---\n${entitySummary}`,
});
throw new Error("Prompt contains PII. Review the redacted preview in the status panel, then confirm " +
"your message with --c to proceed.");
}
// ------------------------------------------------------------------
// No confirmation required β forward redacted text immediately
// ------------------------------------------------------------------
detectionStatus.setState({
status: "done",
text: `PII redacted successfully.\n\n${entitySummary}`,
});
ctl.debug("Redaction result", result);
return result.redacted_text;
}