| | 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 }; |
| |
|