| import axios from "axios"; |
| import config from "../config.js"; |
| import supabase from "../lib/supabase.js"; |
| import { getJob, updateJobStatus } from "./jobService.js"; |
|
|
| const INJECTION_KEYWORDS = [ |
| "ignore", |
| "override", |
| "set score", |
| "approve this", |
| "reject this", |
| "system prompt", |
| "you are now", |
| "disregard", |
| "forget previous", |
| "new instructions", |
| "jailbreak", |
| "ignore all", |
| "ignore previous", |
| ]; |
|
|
| function computeDecision(score) { |
| if (score >= 75) return "APPROVE"; |
| if (score >= 55) return "CONDITIONAL"; |
| return "REJECT"; |
| } |
|
|
| async function processOfficerNotes(jobId, rawNotes, officerId) { |
| |
| let sanitizedNotes = rawNotes.trim(); |
| if (sanitizedNotes.length > 2000) { |
| throw new Error("Notes too long. Maximum 2000 characters."); |
| } |
| |
| sanitizedNotes = sanitizedNotes.replace(/<[^>]*>/g, ""); |
|
|
| |
| const lower = sanitizedNotes.toLowerCase(); |
| const injectionFound = INJECTION_KEYWORDS.some((kw) => lower.includes(kw)); |
|
|
| if (injectionFound) { |
| |
| await supabase.from("audit_log").insert({ |
| job_id: jobId, |
| event_type: "INJECTION_ATTEMPT", |
| event_data: { officer_id: officerId, raw_text: rawNotes }, |
| }); |
|
|
| |
| await supabase.from("officer_notes").insert({ |
| job_id: jobId, |
| raw_text: rawNotes, |
| injection_detected: true, |
| score_delta: -50, |
| officer_id: officerId, |
| }); |
|
|
| |
| const job = await getJob(jobId); |
| const currentScore = job?.result?.score_breakdown?.final_score ?? 0; |
| const currentDecision = job?.result?.score_breakdown?.decision ?? "REJECT"; |
| const newScore = Math.max(0, currentScore - 50); |
|
|
| return { |
| injection_detected: true, |
| injection_message: |
| "Prompt injection attempt detected. Penalty: -50 points. Incident logged to compliance.", |
| score_before: currentScore, |
| score_delta: -50, |
| score_after: newScore, |
| decision_before: currentDecision, |
| decision_after: computeDecision(newScore), |
| adjustments: [], |
| interpretation: "Security violation detected.", |
| }; |
| } |
|
|
| |
| const job = await getJob(jobId); |
| if (!job) throw new Error("Job not found"); |
| const currentScore = job.result?.score_breakdown?.final_score ?? 0; |
| const currentDecision = job.result?.score_breakdown?.decision ?? "REJECT"; |
|
|
| |
| const aiRes = await axios.post( |
| `${config.aiServiceUrl}/api/v1/cam/officer-notes`, |
| { |
| job_id: jobId, |
| notes_text: sanitizedNotes, |
| officer_id: officerId, |
| }, |
| { timeout: 60000 }, |
| ); |
| const aiResponse = aiRes.data; |
| const scoreDelta = aiResponse.score_delta ?? 0; |
|
|
| |
| const newScore = Math.max(0, Math.min(100, currentScore + scoreDelta)); |
| const newDecision = computeDecision(newScore); |
|
|
| const updatedResult = { |
| ...job.result, |
| score_breakdown: { |
| ...job.result.score_breakdown, |
| final_score: newScore, |
| decision: newDecision, |
| }, |
| officer_notes_applied: true, |
| officer_score_delta: scoreDelta, |
| }; |
|
|
| await updateJobStatus(jobId, "completed", updatedResult); |
|
|
| |
| await supabase.from("officer_notes").insert({ |
| job_id: jobId, |
| raw_text: sanitizedNotes, |
| injection_detected: false, |
| score_delta: scoreDelta, |
| officer_id: officerId, |
| }); |
|
|
| await supabase.from("audit_log").insert({ |
| job_id: jobId, |
| event_type: "OFFICER_NOTES_SUBMITTED", |
| event_data: { |
| officer_id: officerId, |
| score_delta: scoreDelta, |
| injection_detected: false, |
| }, |
| }); |
|
|
| |
| return { |
| injection_detected: false, |
| score_before: currentScore, |
| score_delta: scoreDelta, |
| score_after: newScore, |
| decision_before: currentDecision, |
| decision_after: newDecision, |
| adjustments: aiResponse.adjustments || [], |
| interpretation: aiResponse.interpretation || "", |
| }; |
| } |
|
|
| export { processOfficerNotes }; |
|
|