Prompt-injection wrap: remaining AI routes

Applied the <UNTRUSTED_*> delimiter + INJECTION_GUARD pattern to:
  - src/routes/sickVisit.js  (chief complaint, transcript, dictation,
                               ROS, physical exam, diagnoses, style hints)
  - src/routes/wellVisit.js  (SSHADESS answers + full well-visit context)
  - src/routes/chartReview.js (PMH + all visit content + labs)
  - src/routes/hospitalCourse.js (all notes/H&P/ED + clarification
                                   & update endpoints)
  - src/routes/milestones.js (narrative + summary)

Each wraps patient-derived text in <UNTRUSTED_*>…</UNTRUSTED_*>
tags and appends the INJECTION_GUARD system instruction that tells
the model to treat wrapped content strictly as data. Operator-
supplied `additionalInstructions` stays unwrapped (trusted).
This commit is contained in:
Daniel 2026-04-14 05:31:54 +02:00
parent 63f77aa9cf
commit ea03db3d45
5 changed files with 60 additions and 90 deletions

View file

@ -4,6 +4,7 @@ const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Generate chart review
router.post('/generate-chart-review', authMiddleware, async (req, res) => {
@ -21,7 +22,7 @@ router.post('/generate-chart-review', authMiddleware, async (req, res) => {
const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
let clinicalData = `Today's date: ${today}\nPatient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`;
if (pmh) clinicalData += `\nPMH: ${pmh}`;
if (pmh) clinicalData += `\nPMH: ${wrapUserText('pmh', pmh)}`;
// Use the top-level review type to select the prompt —
// the per-visit types only control data formatting, not the prompt.
@ -37,29 +38,29 @@ router.post('/generate-chart-review', authMiddleware, async (req, res) => {
// Include ALL visit data regardless of per-visit type —
// an outpatient chart review should include subspecialty consults too
(visits || []).forEach(v => {
clinicalData += `\n\n=== OUTPATIENT VISIT (${v.date}) ===\n${v.content}`;
if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n${v.labs}`;
clinicalData += `\n\n=== OUTPATIENT VISIT (${v.date}) ===\n` + wrapUserText('visit', v.content || '');
if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
});
(subspecialty || []).forEach(s => {
clinicalData += `\n\n=== SUBSPECIALTY: ${(s.specialty || 'Subspecialty').toUpperCase()}${s.specialistName || 'Unknown'} (${s.date}) ===\n${s.content}`;
if (s.labs && s.labs.trim()) clinicalData += `\n--- Labs from this visit (${s.date}) ---\n${s.labs}`;
clinicalData += `\n\n=== SUBSPECIALTY: ${(s.specialty || 'Subspecialty').toUpperCase()}${s.specialistName || 'Unknown'} (${s.date}) ===\n` + wrapUserText('consult', s.content || '');
if (s.labs && s.labs.trim()) clinicalData += `\n--- Labs from this visit (${s.date}) ---\n` + wrapUserText('labs', s.labs);
});
(edVisits || []).forEach(v => {
clinicalData += `\n\n=== ED VISIT (${v.date}) ===\n${v.content}`;
if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n${v.labs}`;
clinicalData += `\n\n=== ED VISIT (${v.date}) ===\n` + wrapUserText('ed_visit', v.content || '');
if (v.labs && v.labs.trim()) clinicalData += `\n--- Labs from this visit (${v.date}) ---\n` + wrapUserText('labs', v.labs);
});
if (labs && labs.length > 0) {
clinicalData += '\n\n=== ADDITIONAL LABS (not tied to a specific visit) ===';
labs.forEach(l => { clinicalData += `\n${l.date ? l.date + ': ' : ''}${l.values}`; });
labs.forEach(l => { clinicalData += `\n${l.date ? l.date + ': ' : ''}` + wrapUserText('labs', l.values || ''); });
}
if (additionalInstructions) {
prompt += `\n\nADDITIONAL INSTRUCTIONS:\n${additionalInstructions}`;
prompt += `\n\nADDITIONAL INSTRUCTIONS (operator-supplied, trusted):\n${additionalInstructions}`;
}
const result = await callAI([
{ role: 'system', content: prompt },
{ role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: clinicalData }
], { model, maxTokens: 4000 });

View file

@ -4,6 +4,7 @@ const { callAI } = require('../utils/ai');
const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Generate hospital course
router.post('/generate-hospital-course', authMiddleware, async (req, res) => {
@ -51,41 +52,35 @@ router.post('/generate-hospital-course', authMiddleware, async (req, res) => {
// Build the complete clinical data
let clinicalData = `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}`;
if (pmh) clinicalData += `\nPMH: ${pmh}`;
if (pmh) clinicalData += `\nPMH: ` + wrapUserText('pmh', pmh);
if (setting) clinicalData += `\nSetting: ${setting}`;
if (los) clinicalData += `\nLength of Stay: ${los} days`;
clinicalData += `\nFormat: ${format}`;
if (edNote) {
clinicalData += `\n\n=== ED NOTE (${edNote.date || 'date unknown'}) ===\n${edNote.content}`;
if (edNote.labs) clinicalData += `\nED Labs: ${edNote.labs}`;
clinicalData += `\n\n=== ED NOTE (${edNote.date || 'date unknown'}) ===\n` + wrapUserText('ed_note', edNote.content || '');
if (edNote.labs) clinicalData += `\nED Labs: ` + wrapUserText('labs', edNote.labs);
}
if (hAndP) clinicalData += `\n\n=== H&P (${hAndP.date || 'date unknown'}) ===\n` + wrapUserText('h_and_p', hAndP.content || '');
if (hAndP) {
clinicalData += `\n\n=== H&P (${hAndP.date || 'date unknown'}) ===\n${hAndP.content}`;
}
// Sort notes by date
const sortedNotes = [...notes].sort((a, b) => new Date(a.date) - new Date(b.date));
sortedNotes.forEach((note, i) => {
clinicalData += `\n\n=== ${(note.type || 'Progress Note').toUpperCase()} - ${note.date || `Note ${i + 1}`} ===\n${note.content}`;
clinicalData += `\n\n=== ${(note.type || 'Progress Note').toUpperCase()} - ${note.date || `Note ${i + 1}`} ===\n` + wrapUserText('note', note.content || '');
});
if (labs && labs.length > 0) {
clinicalData += '\n\n=== LABS ===';
labs.forEach(lab => {
clinicalData += `\n${lab.date}: ${lab.values}`;
});
labs.forEach(lab => { clinicalData += `\n${lab.date}: ` + wrapUserText('labs', lab.values || ''); });
}
if (additionalInstructions) {
prompt += `\n\nADDITIONAL INSTRUCTIONS FROM PHYSICIAN:\n${additionalInstructions}`;
prompt += `\n\nADDITIONAL INSTRUCTIONS FROM PHYSICIAN (operator-supplied, trusted):\n${additionalInstructions}`;
}
if (physicianMemories) clinicalData += '\n\n[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + physicianMemories + '\n[END STYLE HINTS]';
if (physicianMemories) clinicalData += '\n\n[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n'
+ wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]';
const result = await callAI([
{ role: 'system', content: prompt },
{ role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: clinicalData }
], { model, maxTokens: 6000 });
@ -107,8 +102,8 @@ router.post('/hospital-course-clarify', authMiddleware, async (req, res) => {
const { currentDraft, notes } = req.body;
const result = await callAI([
{ role: 'system', content: PROMPTS.askClarification },
{ role: 'user', content: `Current draft:\n${currentDraft}\n\nSource notes:\n${JSON.stringify(notes)}` }
{ role: 'system', content: PROMPTS.askClarification + INJECTION_GUARD },
{ role: 'user', content: `Current draft:\n` + wrapUserText('draft', currentDraft || '') + `\n\nSource notes:\n` + wrapUserText('notes', JSON.stringify(notes || [])) }
], { model: req.body.model });
res.json({ success: true, questions: result.content, model: result.model });
@ -123,15 +118,15 @@ router.post('/hospital-course-update', authMiddleware, async (req, res) => {
const { currentCourse, updates, model, instructions } = req.body;
if (!currentCourse) return res.status(400).json({ error: 'No current course' });
let userMsg = `CURRENT HOSPITAL COURSE:\n${currentCourse}\n\n`;
if (updates) userMsg += `NEW UPDATES TO ADD:\n${updates}\n\n`;
if (instructions) userMsg += `INSTRUCTIONS:\n${instructions}`;
let userMsg = `CURRENT HOSPITAL COURSE:\n` + wrapUserText('course', currentCourse) + `\n\n`;
if (updates) userMsg += `NEW UPDATES TO ADD:\n` + wrapUserText('updates', updates) + `\n\n`;
if (instructions) userMsg += `INSTRUCTIONS:\n` + wrapUserText('instructions', instructions);
const result = await callAI([
{ role: 'system', content: `You are updating a hospital course document with new information.
Add the new information in the appropriate place. Maintain the same format and style.
If adding discharge day information, add it at the end.
${PROMPTS.refine.split('\n').slice(0, -1).join('\n')}` },
${PROMPTS.refine.split('\n').slice(0, -1).join('\n')}` + INJECTION_GUARD },
{ role: 'user', content: userMsg }
], { model, maxTokens: 6000 });

View file

@ -5,6 +5,7 @@ const PROMPTS = require('../utils/prompts');
const { authMiddleware } = require('../middleware/auth');
const db = require('../db/database');
var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Get all milestones for client (authenticated users)
router.get('/milestones-data', authMiddleware, async (req, res) => {
@ -59,8 +60,8 @@ router.post('/generate-milestone-narrative', authMiddleware, async (req, res) =>
const prompt = (format === 'list') ? PROMPTS.milestoneList : PROMPTS.milestoneNarrative;
const result = await callAI([
{ role: 'system', content: prompt },
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\n${milestoneText}\n\nAssessed: ${achieved.length + notAchieved.length} | Achieved: ${achieved.length} | Not achieved: ${notAchieved.length} | Not assessed (OMIT): ${notAssessed.length}` }
{ role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\n` + wrapUserText('milestones', milestoneText) + `\n\nAssessed: ${achieved.length + notAchieved.length} | Achieved: ${achieved.length} | Not achieved: ${notAchieved.length} | Not assessed (OMIT): ${notAssessed.length}` }
], { model });
res.json({
@ -82,8 +83,8 @@ router.post('/generate-milestone-summary', authMiddleware, async (req, res) => {
if (!narrative) return res.status(400).json({ error: 'No narrative' });
const result = await callAI([
{ role: 'system', content: PROMPTS.milestoneSummary },
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n${narrative}` }
{ role: 'system', content: PROMPTS.milestoneSummary + INJECTION_GUARD },
{ role: 'user', content: `Patient: ${patientAge || 'Unknown'}, ${patientGender || 'Unknown'}\nAge Group: ${ageGroup}\n\nNARRATIVE:\n` + wrapUserText('narrative', narrative) }
], { model, maxTokens: 500 });
res.json({ success: true, summary: result.content, model: result.model });

View file

@ -8,6 +8,7 @@ var { callAI } = require('../utils/ai');
var PROMPTS = require('../utils/prompts');
var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// ── POST generate sick visit note ────────────────────────────────────────
router.post('/sick-visit/note', authMiddleware, async function(req, res) {
@ -28,32 +29,23 @@ router.post('/sick-visit/note', authMiddleware, async function(req, res) {
// Assemble context
var context = 'SICK VISIT\n';
context += 'Patient: ' + (patientAge || 'Unknown age') + ', ' + (patientGender || 'Unknown gender') + '\n';
context += 'Chief Complaint: ' + chiefComplaint + '\n\n';
context += 'Chief Complaint: ' + wrapUserText('chief_complaint', chiefComplaint) + '\n\n';
if (transcript) {
context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + transcript + '\n\n';
context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
} else if (dictation) {
context += 'PHYSICIAN DICTATION:\n' + dictation + '\n\n';
context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
}
if (ros) {
context += ros + '\n\n';
}
if (physicalExam) {
context += physicalExam + '\n\n';
}
if (diagnoses) {
context += diagnoses + '\n\n';
}
if (ros) context += wrapUserText('ros', ros) + '\n\n';
if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
if (physicianMemories) {
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + physicianMemories + '\n[END STYLE HINTS]\n\n';
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n'
+ wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]\n\n';
}
var result = await callAI([
{ role: 'system', content: PROMPTS.sickVisitNote },
{ role: 'system', content: PROMPTS.sickVisitNote + INJECTION_GUARD },
{ role: 'user', content: context }
], { model });

View file

@ -8,6 +8,7 @@ var { callAI } = require('../utils/ai');
var PROMPTS = require('../utils/prompts');
var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
// Load growth reference and BMI classification data
var scheduleData;
@ -112,8 +113,8 @@ router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
}
var result = await callAI([
{ role: 'system', content: PROMPTS.shadessAssessment },
{ role: 'user', content: inputText }
{ role: 'system', content: PROMPTS.shadessAssessment + INJECTION_GUARD },
{ role: 'user', content: wrapUserText('sshadess_answers', inputText) }
], { model });
var dur = Date.now() - start;
@ -155,40 +156,20 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) {
var context = 'WELL CHILD VISIT\n';
context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n';
if (vitals) {
context += 'VITAL SIGNS:\n' + vitals + '\n\n';
}
if (measurements) {
context += 'MEASUREMENTS/GROWTH:\n' + measurements + '\n\n';
}
if (parentConcerns) {
context += 'PARENT/PATIENT CONCERNS:\n' + parentConcerns + '\n\n';
}
if (transcript) {
context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + transcript + '\n\n';
} else if (dictation) {
context += 'PHYSICIAN DICTATION:\n' + dictation + '\n\n';
}
if (screenings) {
context += 'SCREENINGS COMPLETED:\n' + screenings + '\n\n';
}
if (vaccines) {
context += 'IMMUNIZATIONS TODAY:\n' + vaccines + '\n\n';
}
if (shadessAssessment) {
context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + shadessAssessment + '\n\n';
}
if (ros) {
context += ros + '\n\n';
}
if (physicalExam) {
context += physicalExam + '\n\n';
}
if (diagnoses) {
context += diagnoses + '\n\n';
}
if (vitals) context += 'VITAL SIGNS:\n' + wrapUserText('vitals', vitals) + '\n\n';
if (measurements) context += 'MEASUREMENTS/GROWTH:\n' + wrapUserText('measurements', measurements) + '\n\n';
if (parentConcerns) context += 'PARENT/PATIENT CONCERNS:\n' + wrapUserText('concerns', parentConcerns) + '\n\n';
if (transcript) context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
else if (dictation) context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
if (screenings) context += 'SCREENINGS COMPLETED:\n' + wrapUserText('screenings', screenings) + '\n\n';
if (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n';
if (shadessAssessment) context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + wrapUserText('sshadess', shadessAssessment) + '\n\n';
if (ros) context += wrapUserText('ros', ros) + '\n\n';
if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
if (diagnoses) context += wrapUserText('diagnoses', diagnoses) + '\n\n';
if (physicianMemories) {
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + physicianMemories + '\n[END STYLE HINTS]\n\n';
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n'
+ wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]\n\n';
}
// Add growth reference and feeding guidance for this age
@ -214,7 +195,7 @@ router.post('/well-visit/note', authMiddleware, async function(req, res) {
var prompt = (noteStyle === 'short') ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
var result = await callAI([
{ role: 'system', content: prompt },
{ role: 'system', content: prompt + INJECTION_GUARD },
{ role: 'user', content: context }
], { model });