Revisionen: C-3, C-4, M-1, M-4, M-5, M-6, m-3, m-5 och m-7 åtgärdade
C-3 · Dataskydd genom krypto-shredding. Identifierande fält krypteras med en nyckel per ärende; radering sker genom att nyckeln förstörs. Loggen förblir intakt och hashverifierbar — det som blir oåtkomligt är identifieringen, inte protokollet över vad som kontrollerades. Ett raderat ärende visar fortfarande att lufttrycket mättes till 2,4 bar klockan 08:42, bara inte längre vems bil det gällde. Vad som inte krypteras är lika viktigt: mätvärden, observationer och kontrollresultat är verksamhetsdata. Krypteras allt raderas beviset tillsammans med personuppgiften. En raderingsbegäran gäller ett fordon, inte ett ärende — men identifieraren är krypterad och går inte att söka på. Därför ett blindat index: HMAC av den normaliserade identifieraren, samma fordon ger alltid samma värde, värdet går inte att vända tillbaka utan nyckeln. Gallringsdatum sätts vid avslut utifrån ärendetypen. Ett ärende utan datum gallras aldrig — för tidig gallring går inte att ångra. C-4 · Modellanropen kan stängas av per organisation. Flaggan bärs i token så orkestern kan neka utan databasåtkomst. Metodikmotorn fungerar ensam; en verkstad som inte kan acceptera överföringen till modelleverantören kan ändå använda produkten. M-1 · Mätvärden bär vilket mätdon som användes och när det var kalibrerat. Utan spårbart instrument nedgraderas värdet från E4 till E1 — det är teknikerns observation av en siffra, inte en mätning. Kalibreringen bedöms vid mättillfället, inte i dag. Demoärendet fick kalibrerade instrument: det ska visa den praxis produkten kräver. M-4 · Läslogg. Varje skrivning loggades redan; ingen läsning gjorde det. M-5 · Återställningstest i CI. Larmet visade att backup sker, inte att den går att återställa. Testet kontrollerar det som faktiskt brukar tappas: att append-only-triggarna följde med och fortfarande biter. M-6 · Regelpaketet verifieras mot HMAC. Ogiltig signatur spärrar avslut; saknad nyckel ger granskningsläge i stället för driftavbrott — det är så säkerhetsfunktioner blir avstängda. m-3 · EXIF-borttagningen är nu avsiktlig och låst av test. Den höll av en slump, och en ändring i stil med "bevara originalkvaliteten" hade tyst börjat publicera var kundens bil stod. m-5 · SBOM och sårbarhetsskanning i CI. m-7 · Promptversionen binds till varje svar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
// PORT default 8080
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { avsluta, logga, mätvärde, spårFrån, starta } from "./observation.mjs";
|
||||
|
||||
@@ -250,6 +250,17 @@ export function verifieraJwt(token, hemlighet) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Kort, stabil identitet för den instruktion som gav svaret: modell,
|
||||
* effort och systempromptens innehåll. Ändras något av dem ändras
|
||||
* versionen, och två svar går att jämföra utan att prompten sparas.
|
||||
*/
|
||||
function promptversion(konfig) {
|
||||
const underlag = `${konfig.modell}|${konfig.effort ?? "-"}|${konfig.system}`;
|
||||
return createHash("sha256").update(underlag).digest("hex").slice(0, 12);
|
||||
}
|
||||
|
||||
function svara(res, status, kropp) {
|
||||
const data = JSON.stringify(kropp);
|
||||
res.writeHead(status, {
|
||||
@@ -306,10 +317,23 @@ export function skapaServer() {
|
||||
|
||||
const auth = req.headers.authorization ?? "";
|
||||
const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
||||
if (!token || !verifieraJwt(token, jwtHemlighet)) {
|
||||
const anspr = token ? verifieraJwt(token, jwtHemlighet) : null;
|
||||
if (!anspr) {
|
||||
return svara(res, 401, { error: "Inloggning krävs." });
|
||||
}
|
||||
|
||||
// Organisationen kan ha stängt av modellanropen helt — normalt för
|
||||
// att den inte kan acceptera överföringen av kund- och fordonsdata
|
||||
// till modelleverantören (QUALITET C-4). Flaggan bärs i token, så
|
||||
// beslutet fattas här och inte av klienten. Metodikmotorn fungerar
|
||||
// ensam; produkten blir mindre hjälpsam, inte obrukbar.
|
||||
if (anspr.ai === false) {
|
||||
return svara(res, 403, {
|
||||
error: "Organisationen har stängt av modellanropen. Metodiken guidar utan dem.",
|
||||
avstangd: true,
|
||||
});
|
||||
}
|
||||
|
||||
let uppgift, prompt, bild;
|
||||
try {
|
||||
({ uppgift, prompt, bild } = await lasKropp(req));
|
||||
@@ -380,7 +404,17 @@ export function skapaServer() {
|
||||
}
|
||||
const textBlock = svar.content.find((block) => block.type === "text");
|
||||
if (!textBlock) return svara(res, 502, { error: "AI-svaret saknade innehåll." });
|
||||
return svara(res, 200, { modell: konfig.modell, svar: JSON.parse(textBlock.text) });
|
||||
return svara(res, 200, {
|
||||
modell: konfig.modell,
|
||||
// Modellen loggades redan; prompten gjorde det inte. Två
|
||||
// identiska ärenden kunde därmed få olika vägledning utan att
|
||||
// skillnaden gick att härleda i efterhand (QUALITET m-7).
|
||||
// Hashen är kort med avsikt — den ska kunna stå i en loggrad och
|
||||
// svara på "kördes de här två svaren mot samma instruktion?",
|
||||
// inte återskapa prompten.
|
||||
promptversion: promptversion(konfig),
|
||||
svar: JSON.parse(textBlock.text),
|
||||
});
|
||||
} catch (fel) {
|
||||
logga("fel", "AI-anropet misslyckades", {
|
||||
uppgift,
|
||||
|
||||
Reference in New Issue
Block a user