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