42 lines
4.0 KiB
JavaScript
42 lines
4.0 KiB
JavaScript
import {z} from 'zod';
|
|
import {inspectForAI,allowsConfidentialLocal} from '../../packages/core/policy.mjs';
|
|
import {inspector} from './inspection.mjs';
|
|
import enterpriseInspection from '../browser/enterprise-inspection.cjs';
|
|
import {traceOperation} from './telemetry.mjs';
|
|
|
|
function rejected(status,message,ruleId){return Object.assign(Error(message),{status,ruleId});}
|
|
export function approvedAIEndpoint(policy,config){
|
|
const a=policy.ai,local=a.trustMode==='local';
|
|
if(!a.enabled||!config.aiBaseUrl||(!local&&!config.aiKey)||a.model!==config.aiModel||a.endpointId!==(config.aiEndpointId||'default'))throw rejected(409,'Approved AI provider is not configured');
|
|
const endpoint=new URL(`${config.aiBaseUrl.replace(/\/$/,'')}/${a.provider==='ollama'?'api/chat':'chat/completions'}`);
|
|
if(endpoint.username||endpoint.password||endpoint.search||endpoint.hash||(local?!(['127.0.0.1','[::1]'].includes(endpoint.hostname)&&['http:','https:'].includes(endpoint.protocol)):endpoint.protocol!=='https:'))throw rejected(403,'AI endpoint trust policy rejected');
|
|
return endpoint;
|
|
}
|
|
export async function runApprovedAI({store,config,deviceId,policy,prompt}){
|
|
return traceOperation('ai.gateway',{policyVersion:policy.version,bytes:Buffer.byteLength(prompt)},async()=>{
|
|
try{
|
|
z.string().min(1).max(100000).parse(prompt);
|
|
if(Date.parse(policy.expiresAt)<=Date.now())throw rejected(403,'Policy expired');
|
|
const endpoint=approvedAIEndpoint(policy,config);
|
|
async function scan(text){
|
|
let verdict=inspectForAI(Buffer.from(text),policy);
|
|
if(verdict.decision==='allow'&&(policy.dlp.semantic.enabled||policy.security.yara.enabled)){
|
|
const parsed=await inspector(config,'/inspect',{data:Buffer.from(text).toString('base64'),policy});
|
|
if(parsed.decision==='block')verdict=allowsConfidentialLocal(parsed,policy)?{...parsed,decision:'allow',classification:'local-only'}:parsed;
|
|
}
|
|
if(verdict.decision==='allow'){const external=await enterpriseInspection.scanEnterprise(Buffer.from(text),policy,'ai');if(external.decision!=='allow')verdict=external;}
|
|
return verdict;
|
|
}
|
|
const input=await traceOperation('ai.inspect.input',{bytes:Buffer.byteLength(prompt),policyVersion:policy.version},()=>scan(prompt));store.event(deviceId,'ai.decision',{...input,policyVersion:policy.version});
|
|
if(input.decision==='block')throw rejected(403,'Company policy blocked this prompt',input.ruleId);
|
|
const response=await traceOperation('ai.provider.request',{bytes:Buffer.byteLength(prompt),policyVersion:policy.version},()=>fetch(endpoint,{method:'POST',redirect:'error',headers:{'content-type':'application/json',...(config.aiKey?{authorization:`Bearer ${config.aiKey}`}:{})},body:JSON.stringify({model:policy.ai.model,messages:[{role:'user',content:prompt}],stream:false}),signal:AbortSignal.timeout(60000)}));
|
|
if(!response.ok)throw rejected(502,'AI provider request failed');
|
|
const text=await traceOperation('ai.provider.read',{policyVersion:policy.version},async()=>{const chunks=[];let size=0;for await(const chunk of response.body){size+=chunk.length;if(size>2*1024*1024){await response.body.cancel().catch(()=>{});throw Error('AI response too large');}chunks.push(chunk);}return Buffer.concat(chunks).toString('utf8');});
|
|
const answer=await traceOperation('ai.provider.decode',{bytes:Buffer.byteLength(text),policyVersion:policy.version},async()=>{const result=JSON.parse(text);return z.string().max(100000).parse(policy.ai.provider==='ollama'?result.message?.content:result.choices?.[0]?.message?.content);});
|
|
const output=await traceOperation('ai.inspect.output',{bytes:Buffer.byteLength(answer),policyVersion:policy.version},()=>scan(answer));store.event(deviceId,'ai.output-decision',{...output,policyVersion:policy.version});
|
|
if(output.decision==='block'){store.event(deviceId,'ai.output-block',{ruleId:output.ruleId,policyVersion:policy.version});throw rejected(403,'AI response blocked by company policy',output.ruleId);}
|
|
return answer;
|
|
}catch(e){store.event(deviceId,'ai.failure',{policyVersion:policy.version});throw e;}
|
|
});
|
|
}
|