@tuent/sentinel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ import{runInitClaudeCode as le}from"./chunk-3U3PKD4N.js";import{AgentProfileManager as H,AlertManager as ce,AuditTrail as A,BaselineBuilder as k,CorrelationDetector as de,DeviationDetector as B,FileStorageBackend as ge,ProfileStore as ue,ReportGenerator as fe,Sentinel as C,SentinelRunner as pe,generateFleetReport as he}from"./chunk-QFRDEISP.js";import"./chunk-CUJKNIKT.js";import"./chunk-FMZWHT4M.js";import"./chunk-6MHWJATS.js";import{loadPolicy as me}from"./chunk-2FFMYSVC.js";import{getOrCreateKeyPair as M}from"./chunk-NUXSUSYY.js";import{join as u}from"path";import{homedir as m}from"os";import{readFile as E,writeFile as b,access as K,mkdir as W}from"fs/promises";function we(e){const o=new Date(e);if(isNaN(o.getTime()))return"unknown";const t=Date.now()-o.getTime();if(t<0)return"just now";const a=Math.floor(t/6e4);if(a<1)return"just now";if(a<60)return`${a} minute${a===1?"":"s"} ago`;const s=Math.floor(a/60);if(s<24)return`${s} hour${s===1?"":"s"} ago`;const r=Math.floor(s/24);if(r<14)return`${r} day${r===1?"":"s"} ago`;const d=Math.floor(r/7);if(r<60)return`${d} week${d===1?"":"s"} ago`;if(r>=365)return"over a year ago";const c=Math.floor(r/30);return`${c} month${c===1?"":"s"} ago`}function ye(e,o){const n=new Date(o),t=isNaN(n.getTime())?1/0:Math.floor((Date.now()-n.getTime())/864e5);return e<.3||t>30?"declining":e>.7&&t<7?"rising":"stable"}function $e(e){return e<.3?"inner":e<.65?"middle":"outer"}function U(e){if(e.length===0)return"No petals selected.";const o=new Map;for(const t of e)o.set(t.id,t.label);const n=[`Selected petals (${e.length}):
3
+ `];for(const t of e){const a=$e(t.layer),s=t.isRichData?"":" [filler]";n.push(`- ${t.label}${s}`),n.push(` Category: ${t.category}`),n.push(` Layer zone: ${a} (${(t.layer*100).toFixed(0)}%)`),n.push(` Openness: ${(t.openness*100).toFixed(0)}%`),n.push(` Description: ${t.description}`),n.push(` Last active: ${t.lastActive}`);const r=we(t.lastActive),d=t.weight!=null?ye(t.weight,t.lastActive):"stable";if(n.push(` Temporal: Last active ${r} | Weight trend: ${d}`),t.source){const c={seed:"seed data",agent:"observed from activity",manual:"filesystem scan",diary:"personal diary entry",conversation:"created from conversation","agent-monitor":"monitored agent activity"};n.push(` Source: ${c[t.source]??t.source}`)}if(t.weight!=null&&n.push(` Weight: ${t.weight.toFixed(2)}`),t.connections.length>0){const c=t.connections.map(i=>o.get(i)??ve(i));n.push(` Connections: ${c.join(", ")}`)}if(t.files&&t.files.length>0){const c=t.files.slice(0,10).map(i=>i.split("/").pop()??i);n.push(` Key files: ${c.join(", ")}`)}if(t.fileContents&&t.fileContents.length>0){n.push(" File contents:");for(const c of t.fileContents)n.push(` --- ${c.name} ---`),n.push(c.content.split(`
4
+ `).map(i=>` ${i}`).join(`
5
+ `))}n.push("")}return n.join(`
6
+ `)}function ve(e){return e.split("-").map(o=>o.charAt(0).toUpperCase()+o.slice(1)).join(" ")}function Ae(e){const o=[Ie(e.agentName,e.agentId,e.role),Se(e.baseline),De(e.e),Te(e.findings),Ce()],n=be(e.t);return n&&o.push(n),o.join(`
7
+
8
+ `)}function Ie(e,o,n){const t=["=== Agent Identity ===",`Name: ${e}`,`ID: ${o}`];if(n){if(t.push(`Role: ${n.description}`),t.push(`Allowed actions: ${n.allowedActions.length>0?n.allowedActions.join(", "):"Not defined"}`),n.forbiddenTargetPatterns.length>0&&t.push(`Forbidden targets: ${n.forbiddenTargetPatterns.join(", ")}`),n.expectedSchedule){const a=[];n.expectedSchedule.activeHours&&a.push(`hours ${n.expectedSchedule.activeHours[0]}-${n.expectedSchedule.activeHours[1]}`),n.expectedSchedule.activeDays&&a.push(`days ${n.expectedSchedule.activeDays.join(", ")}`),a.length>0&&t.push(`Expected schedule: ${a.join("; ")}`)}}else t.push("No role definition \u2014 monitoring behavioral baseline only");return t.join(`
9
+ `)}function Se(e){if(!e)return`=== Behavioral Baseline ===
10
+ Not yet established. Insufficient data for baseline computation.`;const o=Object.entries(e.actionDistribution).map(([t,a])=>`${t}: ${a}%`).join(", ");return["=== Behavioral Baseline ===",`Period: ${e.periodDays} days`,`Total sessions: ${e.totalSessions}`,`Total events: ${e.totalEvents}`,`Average events per session: ${e.averageEventsPerSession}`,`Average session duration: ${e.averageSessionDurationMinutes} minutes`,`Typical active hours: ${e.typicalActiveHours[0]}-${e.typicalActiveHours[1]}`,`Typical active days: ${e.typicalActiveDays.length>0?e.typicalActiveDays.join(", "):"N/A"}`,`Action distribution: ${o||"N/A"}`,`Normal weight range: ${e.normalWeightRange[0].toFixed(2)}-${e.normalWeightRange[1].toFixed(2)}`,`Top targets: ${e.topTargets.length>0?e.topTargets.slice(0,10).join(", "):"N/A"}`].join(`
11
+ `)}function De(e){const o=`=== Recent Activity (${e.length} sessions) ===`;return e.length===0?`${o}
12
+ No recent activity recorded.`:`${o}
13
+ ${U(e)}`}function Te(e){const o="=== Security Findings ===";if(e.length===0)return`${o}
14
+ No security findings detected. Agent behavior is within expected parameters.`;const n=[o];for(const t of e)n.push(`[${t.severity}] ${t.type}`),n.push(` Description: ${t.description}`),n.push(` Evidence: ${t.evidence.action} \u2192 ${t.evidence.target} at ${t.evidence.timestamp}`),t.evidence.baselineComparison&&n.push(` Baseline comparison: ${t.evidence.baselineComparison}`),n.push(` Recommendation: ${t.recommendation}`),n.push("");return n.join(`
15
+ `)}function Ce(){return["=== Analysis Request ===","Based on the data above, provide:","1. OVERALL HEALTH ASSESSMENT: Is this agent operating normally?","2. RISK SUMMARY: What is the overall risk level?","3. FINDING REVIEW: For each finding, confirm or adjust its severity with reasoning.","4. RECOMMENDATIONS: Specific actions the owner should take.","5. MONITORING GUIDANCE: What patterns to watch for going forward."].join(`
16
+ `)}function be(e){return!e||e.length===0?null:`=== Owner Context ===
17
+ ${U(e)}`}var f=process.argv.slice(2),p=y(f,"--agent"),G=y(f,"--name"),Re=y(f,"--role"),Me=f.includes("--create"),Ee=f.includes("--compute-baseline"),je=f.includes("--monitor"),Ne=f.includes("--init-config"),ke=f.includes("--init-policy"),Fe=f.includes("init")&&f.includes("claude-code"),xe=f.includes("--force"),_=y(f,"--from-policy"),Pe=f.includes("--report"),Le=f.includes("--report-all"),Oe=f.includes("--correlations"),qe=f.includes("--verify-audit"),He=f.includes("--enroll-manifest"),Be=f.includes("--recompute-stats"),Ke=f.includes("--health"),We=f.includes("--restrict"),Ue=f.includes("--quarantine"),Ge=f.includes("--release"),_e=f.includes("--status"),Je=f.includes("--start-task"),Qe=f.includes("--end-task"),Ve=f.includes("--intent-status"),ze=f.includes("--intent-report"),J=y(f,"--task-id"),Q=y(f,"--description"),V=y(f,"--relaxed-actions"),z=y(f,"--phases"),Y=y(f,"--reason"),F=y(f,"--quarantine"),x=y(f,"--quarantine-reason"),j=y(f,"--period"),X=y(f,"--format"),S=new H;function y(e,o){const n=e.find(a=>a.startsWith(o+"="));if(!n)return;const t=n.indexOf("=");return n.slice(t+1)}async function T(e,o){try{const n=await E(u(e,o,"mode.json"),"utf-8"),t=JSON.parse(n);return t.mode==="restricted"||t.mode==="quarantined"||t.mode==="normal"?t:{mode:"normal"}}catch{return{mode:"normal"}}}function Z(e){const o=["Agent","Score","Status","Mode","C/H/M/L","Last Event","Baseline"],n=e.map(i=>[i.agentId,String(i.score),i.status,i.mode,i.findings,i.lastEvent,i.baseline?"\u2713":"\u2717"]),t=[o,...n],a=o.map((i,l)=>Math.max(...t.map(g=>g[l].length))),s=(i,l)=>i+" ".repeat(l-i.length),r=o.map((i,l)=>s(i,a[l])).join(" "),d=a.map(i=>"-".repeat(i)).join(" "),c=n.map(i=>i.map((l,g)=>s(l,a[g])).join(" "));return[r,d,...c].join(`
18
+ `)}function ee(e){const o=["Agent ID","Mode","Reason","Changed"],n=e.map(i=>[i.agentId,i.mode.toUpperCase(),i.reason,i.changed]),t=[o,...n],a=o.map((i,l)=>Math.max(...t.map(g=>g[l].length))),s=(i,l)=>i+" ".repeat(l-i.length),r=o.map((i,l)=>s(i,a[l])).join(" "),d=a.map(i=>"-".repeat(i)).join(" "),c=n.map(i=>i.map((l,g)=>s(l,a[g])).join(" "));return[r,d,...c].join(`
19
+ `)}function te(e){const o=["Agent ID","Sessions","Last Active","Role","Baseline","Mode","Status"],n=e.map(i=>[i.agentId,String(i.sessions),i.lastActive,i.roleDefined?"\u2713":"\u2717",i.baselineDefined?"\u2713":"\u2717",i.mode??"normal",i.status]),t=[o,...n],a=o.map((i,l)=>Math.max(...t.map(g=>g[l].length))),s=(i,l)=>i+" ".repeat(l-i.length),r=o.map((i,l)=>s(i,a[l])).join(" "),d=a.map(i=>"-".repeat(i)).join(" "),c=n.map(i=>i.map((l,g)=>s(l,a[g])).join(" "));return[r,d,...c].join(`
20
+ `)}function P(e,o){return e<10?"New":o.some(n=>n.severity==="HIGH"||n.severity==="CRITICAL")?"At Risk":o.some(n=>n.severity==="MEDIUM")?"Caution":"Healthy"}function L(e,o){const n=Date.now()-o*24*60*60*1e3;return e.filter(t=>new Date(t.lastActive).getTime()>=n)}function ne(e){return{id:e.id,label:e.label,category:e.category,description:e.description,layer:e.layer,lastActive:e.lastActive,openness:e.openness,connections:e.connections,isRichData:!0,source:e.source,core:e.core,sharable:e.sharable,weight:e.weight,files:e.files}}function O(e,o){const n=["Sentinel \u2014 Live Monitoring","\u2500".repeat(26)];for(const[t,a]of e){const s=(o.get(t)??"manual").toUpperCase(),r=a.getFindings(),d=r.filter(w=>w.severity==="HIGH"||w.severity==="CRITICAL").length,c=r.filter(w=>w.severity==="MEDIUM").length;let i=`${r.length} findings`;d>0?i=`${d} HIGH`:c>0&&(i=`${c} MEDIUM`);const l=P(a.sessionCount,r),g=t.padEnd(18),h=s.padEnd(5);n.push(`[${g}] ${h}| ${a.eventCount} events | ${a.sessionCount} sessions | ${i} | ${l}`)}return n.push(""),n.push("Press Ctrl+C to stop."),n.join(`
21
+ `)}function oe(e){const o=["Monitoring stopped. Summary:"];for(const[n,t]of e){const a=t.getFindings();let s=`${a.length} findings`;if(a.length>0){const r={};for(const c of a)r[c.severity]=(r[c.severity]??0)+1;const d=Object.entries(r).map(([c,i])=>`${i} ${c}`);s=`${a.length} finding${a.length>1?"s":""} (${d.join(", ")})`}o.push(`- ${n}: ${t.eventCount} events, ${t.sessionCount} sessions, ${s}`)}return o.join(`
22
+ `)}async function Ye(e,o){const n=u(m(),".dahlia","agents"),t=await T(n,e);if(t.mode==="quarantined"){console.log(`Agent ${e} is quarantined \u2014 cannot downgrade to restricted.`);return}const a=t.mode;await W(u(n,e),{recursive:!0}),await b(u(n,e,"mode.json"),JSON.stringify({mode:"restricted",reason:o,timestamp:new Date().toISOString(),previousMode:a}),"utf-8");const s=await M(u(n,e)),r=new A(e,{logDir:u(n,e)});await r.open(),r.setSigningKey(s.privateKey,s.publicKey),await r.logModeChange("restricted",o,a),await r.close(),console.log(`Agent ${e} RESTRICTED: ${o}`)}async function Xe(e,o){const n=u(m(),".dahlia","agents"),a=(await T(n,e)).mode;await W(u(n,e),{recursive:!0}),await b(u(n,e,"mode.json"),JSON.stringify({mode:"quarantined",reason:o,timestamp:new Date().toISOString(),previousMode:a}),"utf-8");const s=await M(u(n,e)),r=new A(e,{logDir:u(n,e)});await r.open(),r.setSigningKey(s.privateKey,s.publicKey),await r.logModeChange("quarantined",o,a),await r.close(),console.log(`Agent ${e} QUARANTINED: ${o}`)}async function Ze(e){const o=u(m(),".dahlia","agents"),n=await T(o,e);if(n.mode==="normal"){console.log(`Agent ${e} is already in normal mode.`);return}const t=n.mode;await b(u(o,e,"mode.json"),JSON.stringify({mode:"normal",reason:"manual release",timestamp:new Date().toISOString(),previousMode:t}),"utf-8");const a=await M(u(o,e)),s=new A(e,{logDir:u(o,e)});await s.open(),s.setSigningKey(a.privateKey,a.publicKey),await s.logModeChange("normal","manual release",t),await s.close(),console.log(`Agent ${e} RELEASED (was ${t})`)}async function et(){const e=await S.listAgents();if(e.length===0){console.log("No monitored agents found.");return}const o=u(m(),".dahlia","agents"),n=[];for(const a of e){const s=await T(o,a);n.push({agentId:a,mode:s.mode,reason:s.reason??"-",changed:s.timestamp?s.timestamp.split("T")[0]:"-"})}console.log(`Agent Mode Status
23
+ `),console.log(ee(n));const t=n.filter(a=>a.mode!=="normal");if(t.length>0){console.log("");for(const a of t){const s=a.mode==="quarantined"?"QUARANTINED":"RESTRICTED";console.log(`\u26A0 ${a.agentId}: ${s}`)}}}async function tt(){const e=await S.listAgents();if(e.length===0){console.log("No monitored agents found. Create one with --create --agent=ID --name=NAME");return}const o=[];for(const t of e)try{const r=(await S.loadAgentProfile(t)).build().filter(I=>I.source==="agent-monitor"),d=r.length,c=r.length>0?r.map(I=>I.lastActive).sort().reverse()[0].split("T")[0]:"never",l=await new k(t).loadBaseline(t),g=await S.loadRole(t),h=[];if(l){const I=L(r,7),N=new B(l,g);for(const $ of I){const D={label:$.label,category:$.category,lastActive:$.lastActive,description:$.description,weight:$.weight,source:$.source,files:$.files,connections:$.connections};h.push(...N.analyzeSession(D))}}const w=u(m(),".dahlia","agents"),R=await T(w,t);o.push({agentId:t,sessions:d,lastActive:c,roleDefined:g!==null,baselineDefined:l!==null,status:P(d,h),mode:R.mode})}catch(a){console.warn(`Error loading agent ${t}:`,a),o.push({agentId:t,sessions:0,lastActive:"error",roleDefined:!1,baselineDefined:!1,status:"Error",mode:"normal"})}const n=o.filter(t=>t.mode&&t.mode!=="normal");if(n.length>0){for(const t of n){const a=t.mode==="quarantined"?"QUARANTINED":"RESTRICTED";console.log(`\u26A0 ${t.agentId}: ${a}`)}console.log("")}console.log(`Sentinel Agent Overview
24
+ `),console.log(te(o))}async function nt(e){const o=await S.loadAgentProfile(e),n=await S.loadRole(e),a=await new k(e).loadBaseline(e),r=o.build().filter(D=>D.source==="agent-monitor"),d=L(r,7),c=d.map(ne),i=[];if(a){const D=new B(a,n);for(const v of d){const re={label:v.label,category:v.category,lastActive:v.lastActive,description:v.description,weight:v.weight,source:v.source,files:v.files,connections:v.connections};i.push(...D.analyzeSession(re))}}const l=u(m(),".dahlia","profile.json"),g=new ge(l),h=new ue({backend:g});let w=[];await h.load()&&(w=h.build().filter(v=>v.core===!0).map(ne));const I=Ae({agentId:e,agentName:e,role:n,baseline:a,e:c,findings:i,t:w}),N=new Date().toISOString().split("T")[0],$=`Sentinel Agent Report \u2014 ${e} \u2014 ${d.length} recent sessions \u2014 generated ${N}
25
+
26
+ `;process.stdout.write($+I)}async function ot(e){const o=u(m(),".dahlia","agents"),n=new A(e,{logDir:u(o,e)});await n.open();const t=new k(e),a=await t.computeBaseline(n);await n.close(),await t.saveBaseline(a),console.log(`Baseline computed for agent: ${e}`),console.log(` Sessions: ${a.totalSessions}`),console.log(` Period: ${a.periodDays} days`),console.log(" Action distribution:");for(const[s,r]of Object.entries(a.actionDistribution))console.log(` ${s}: ${r}%`);console.log(` Saved to: ~/.dahlia/agents/${e}/baseline.json`)}async function at(e,o,n){let t;if(n){let a;try{a=await E(n,"utf-8")}catch(s){console.error(`Failed to read role file "${n}":`,s);return}try{t=JSON.parse(a)}catch(s){console.error(`Invalid JSON in role file "${n}":`,s.message);return}}await S.createAgent(e,o,t),console.log(`Agent created: ${e} (${o})`),t&&console.log(` Role loaded from: ${n}`)}async function st(){const e=u(m(),".dahlia","sentinel.json");try{await K(e),console.log(`Config already exists at ${e}`);return}catch{}await b(e,JSON.stringify({agents:[{agentId:"example-agent",name:"Example Agent",adapterType:"log",logPath:"/path/to/agent/activity.log",logFormat:"json-lines",roleDefinitionPath:"~/.dahlia/agents/example-agent/role.json"}],baselineWindowDays:30},null,2)+`
27
+ `),console.log("Template config created at ~/.dahlia/sentinel.json \u2014 edit it with your agent details then run: npm run sentinel -- --monitor")}async function q(){const e=u(m(),".dahlia","sentinel.json");try{const o=await E(e,"utf-8");return JSON.parse(o)}catch(o){if(o.code==="ENOENT")return null;if(o instanceof SyntaxError)return console.error(`Invalid JSON in sentinel config: ${o.message}`),null;throw o}}async function it(e){const o=await q();if(!o){console.log("No sentinel config found. Create ~/.dahlia/sentinel.json or use --init-config to add agents. See docs for config format.");return}let n=o.agents;if(e&&(n=n.filter(l=>l.agentId===e),n.length===0)){console.log(`Agent "${e}" not found in sentinel.json`);return}const t=new Map,a=new Map,s=l=>l&&l.startsWith("~/")?m()+l.slice(1):l;let r;if(o.alerts){const l={...o.alerts.minSeverity&&{minSeverity:o.alerts.minSeverity},...o.alerts.dedupeWindowMs!==void 0&&{dedupeWindowMs:o.alerts.dedupeWindowMs},...o.alerts.quietHoursEnabled!==void 0&&{quietHoursEnabled:o.alerts.quietHoursEnabled},...o.alerts.quietHours&&{quietHours:o.alerts.quietHours},...o.alerts.channels&&{channels:o.alerts.channels}};r=new ce(l)}for(const l of n)try{const g=new pe(l.agentId,void 0,{type:l.adapterType,logPath:s(l.logPath),logFormat:l.logFormat,fieldMapping:l.fieldMapping,mcpLogDir:s(l.mcpLogDir),webhookPort:l.webhookPort,webhookApiKey:l.webhookApiKey,readExisting:l.readExisting});g.setAuditLogDir(u(m(),".dahlia","agents",l.agentId)),g.setFindingCallback(h=>{(h.severity==="HIGH"||h.severity==="CRITICAL")&&console.log(`
28
+ \u26A0 [${h.agentId}] ${h.severity}: ${h.description}`)}),r&&g.setAlertManager(r),await g.start(),t.set(l.agentId,g),a.set(l.agentId,l.adapterType)}catch(g){console.error(`Failed to start monitoring for agent ${l.agentId}:`,g)}console.log(O(t,a));const d=setInterval(()=>{console.clear(),console.log(O(t,a))},5e3),c=async()=>{clearInterval(d);for(const[l,g]of t)try{await g.stop()}catch(h){console.warn(`Error stopping runner ${l}:`,h)}console.log(`
29
+ `+oe(t)),process.exit(0)},i=()=>{c().catch(l=>{console.error("Error during shutdown:",l),process.exit(1)})};process.on("SIGINT",i),process.on("SIGTERM",i)}async function rt(e,o,n){const a=await new fe(e).generateReport({periodDays:o,format:n});process.stdout.write(a)}async function lt(e,o){const n=await he({periodDays:e,format:o});process.stdout.write(n)}async function ct(){const e=await q();if(!e){console.log("No sentinel config found. Create ~/.dahlia/sentinel.json or use --init-config.");return}const o=u(m(),".dahlia","agents"),n=new Map;try{for(const s of e.agents){const r=new A(s.agentId,{logDir:u(o,s.agentId)});await r.open(),n.set(s.agentId,r)}const a=await new de().detect(n);if(a.length===0){console.log("No cross-agent correlations detected.");return}console.log(`Found ${a.length} cross-agent correlation(s):
30
+ `);for(const s of a)console.log(`[${s.severity}] ${s.rule}`),console.log(` Agent A: ${s.agentA.agentId} \u2014 ${s.agentA.action} on ${s.agentA.target}`),console.log(` Agent B: ${s.agentB.agentId} \u2014 ${s.agentB.action} on ${s.agentB.target}`),console.log(` Time delta: ${Math.round(s.timeDeltaMs/1e3)}s`),console.log(` Recommendation: ${s.recommendation}`),console.log("")}finally{for(const t of n.values())await t.close()}}async function dt(e){const o=u(m(),".dahlia","agents"),n=u(o,e,"cumulative-stats.json");let t="(none)";try{t=JSON.parse(await E(n,"utf-8")).totalEntries}catch{}const s=await new A(e,{logDir:u(o,e)}).recomputeCumulativeStats();console.log(`Recomputed cumulative-stats for ${e}:`),console.log(` totalEntries: ${t} \u2192 ${s.totalEntries}`),console.log(` scope: ${s.countScope}`),console.log(" (Read-only over the signed trail; only cumulative-stats.json rewritten.)")}async function gt(e){const o=u(m(),".dahlia","agents"),n=u(o,e),t=await M(n),a=new A(e,{logDir:n});a.setSigningKey(t.privateKey,t.publicKey);const s=F&&x?[{file:F,reason:x}]:void 0,r=await a.enrollManifest(s?{quarantine:s}:void 0);console.log(`Manifest enrolled for ${e}: ${r.records} file record(s) signed + chained`+(r.quarantined?`, ${r.quarantined} quarantine record(s)`:"")+"."),s&&console.log(` Quarantined: ${F} \u2014 ${x}`),console.log(" (Read-only over audit entries; protects from enrollment forward.)")}async function ut(e){const o=u(m(),".dahlia","agents"),n=new A(e,{logDir:u(o,e)});await n.open();const t=await n.verify();if(t.valid?console.log(`Audit trail for ${e}: VALID (${t.totalEntries} entries verified)`):t.brokenAt!==void 0?console.log(`Audit trail for ${e}: BROKEN at entry ${t.brokenAt} (${t.totalEntries} entries checked)`):console.log(`Audit trail for ${e}: INVALID \u2014 file-level manifest integrity failure (${t.totalEntries} entries chain-verified)`),t.manifest){const s=t.manifest;console.log(` Manifest: ${s.recordCount} file record(s) \u2014 ${s.ok?"OK":`${s.issues.length} issue(s)`}`);for(const r of s.issues)console.log(` [${r.type}] ${r.detail}`);for(const r of s.quarantined)console.log(` [QUARANTINED] ${r.file} \u2014 ${r.reason} (retained on disk, excluded from verdict)`)}const a=await n.query({type:"finding",limit:1e4});if(a.length>0){let s=0,r=0,d=0;for(const i of a){const l=i.decision;l==="deny"?r++:l==="modify"?d++:s++}console.log(` Findings: ${a.length} total \u2014 ${s} allowed, ${r} denied, ${d} modified`);const c=a.slice(0,5);for(const i of c){const l=i,g=l.decision,h=l.modification,w=l.description,R=l._decisionDefaulted===!0;g==="modify"&&h?.type==="append_args"?console.log(` [modified] ${w} \u2014 appended args: [${h.args.join(", ")}]`):console.log(g==="deny"?` [denied] ${w}`:R?` [allowed (legacy)] ${w}`:` [allowed] ${w}`)}}await n.close()}async function ft(){const e=u(m(),".dahlia","agents"),o=new C({agentsDir:e}),t=await new H(e).listAgents();if(t.length===0){console.log("No monitored agents found."),await o.stop();return}const a=[];for(const s of t){const r=await o.getHealthScore(s);a.push({agentId:s,score:r.score,status:r.status,mode:r.mode,findings:`${r.findings.critical}/${r.findings.high}/${r.findings.medium}/${r.findings.low}`,lastEvent:r.lastEvent?r.lastEvent.split("T")[0]:"-",baseline:r.baselineEstablished})}await o.stop(),console.log(`Agent Health Dashboard
31
+ `),console.log(Z(a))}var ae=`# Sentinel \u2014 Agent Security Policy
32
+ version: "1.0"
33
+
34
+ agent:
35
+ id: my-agent # unique agent identifier
36
+ name: My AI Agent # human-readable name
37
+ # description: optional
38
+
39
+ policy:
40
+ allow:
41
+ actions: # what the agent can do
42
+ - file_read
43
+ - file_write
44
+ - api_call
45
+ - tool_invocation
46
+ targets: # allowed target patterns
47
+ - "src/**"
48
+ - "docs/**"
49
+ forbid:
50
+ targets: # always denied
51
+ - "**/.env"
52
+ - "**/.ssh/**"
53
+ - "**/.aws/**"
54
+ - "/etc/**"
55
+ - "**/secrets/**"
56
+ # schedule:
57
+ # hours: [9, 18]
58
+ # days: [Monday, Tuesday, Wednesday, Thursday, Friday]
59
+ # limits:
60
+ # maxEventsPerHour: 500
61
+
62
+ # enforcement:
63
+ # restrictAfter: 2
64
+ # quarantineAfter: 3
65
+ # approvalRequired: false
66
+
67
+ # alerts:
68
+ # channels: [console]
69
+ # minSeverity: MEDIUM
70
+
71
+ # repo:
72
+ # repoRoot: . # scan this directory for sensitive files
73
+ # mapPath: ~/.dahlia/repo-sensitivity.json
74
+ # overlayPath: ~/.dahlia/repo-sensitivity.review.json
75
+ `;async function se(){const e=u(process.cwd(),".sentinel.yaml");try{await K(e),console.log(`.sentinel.yaml already exists in ${process.cwd()}`);return}catch{}await b(e,ae,"utf-8"),console.log("Created .sentinel.yaml \u2014 edit then run: npm run sentinel -- --from-policy .sentinel.yaml")}async function ie(e){const o=await me(e),n=await C.fromPolicy(e);console.log(`Loaded policy for agent ${o.agent.id}. Monitoring active.`);const t=async()=>{await n.stop(),console.log("Monitoring stopped."),process.exit(0)},a=()=>{t().catch(s=>{console.error("Error during shutdown:",s),process.exit(1)})};process.on("SIGINT",a),process.on("SIGTERM",a)}async function pt(e,o,n){const t=u(m(),".dahlia","agents"),a=new C({agentsDir:t});await a.addAgent(e,e);const s=V?V.split(","):void 0,r=z?z.split(",").map(c=>c.trim()):void 0,d=a.startTask(e,o,n,{relaxedActions:s,phases:r});if(!d){console.log(`Failed to start task for agent ${e}.`),await a.stop();return}console.log(`Task started for agent ${e}:`),console.log(` Task ID: ${d.taskId}`),console.log(` Description: ${d.description}`),console.log(` Keywords: ${d.keywords.join(", ")}`),d.relaxedActions?.length&&console.log(` Relaxed: ${d.relaxedActions.join(", ")}`),d.phases?.length&&console.log(` Phases: ${d.phases.join(", ")}`),console.log(` TTL: ${(d.ttlMs/6e4).toFixed(0)} minutes`),await a.stop()}async function ht(e){const o=u(m(),".dahlia","agents"),n=new C({agentsDir:o});await n.addAgent(e,e);const t=n.endTask(e);t?(console.log(`Task ended for agent ${e}:`),console.log(` Task ID: ${t.taskId}`),console.log(` Description: ${t.description}`),console.log(` Status: ${t.status}`)):console.log(`No active task found for agent ${e}.`),await n.stop()}async function mt(e){const o=u(m(),".dahlia","agents"),n=new C({agentsDir:o});await n.addAgent(e,e);const t=n.getActiveTask(e);if(!t){console.log(`No active task for agent ${e}.`),await n.stop();return}const a=Date.now()-new Date(t.startedAt).getTime(),s=t.ttlMs-a;console.log(`Active task for agent ${e}:
76
+ `),console.log(` Task ID: ${t.taskId}`),console.log(` Description: ${t.description}`),console.log(` Keywords: ${t.keywords.join(", ")}`),console.log(` Status: ${t.status}`),console.log(` Active for: ${(a/6e4).toFixed(1)} minutes`),console.log(` TTL remain: ${s>0?(s/6e4).toFixed(1)+" minutes":"EXPIRED"}`),t.relaxedActions?.length&&console.log(` Relaxed: ${t.relaxedActions.join(", ")}`),t.phases?.length&&console.log(` Phases: ${t.phases.join(", ")}`),t.acceptableActions.length>0&&console.log(` Acceptable: ${t.acceptableActions.map(r=>`${r.action}:${r.targetPattern}`).join(", ")}`),await n.stop()}async function wt(e){const o=u(m(),".dahlia","agents"),n=new A(e,{logDir:u(o,e)});await n.open();const t=await n.getStats(),a=await n.query({type:"intent_check"}),r=(await n.query({type:"finding"})).filter(c=>typeof c.data=="object"&&c.data!==null&&"type"in c.data&&c.data.type==="intent_drift");console.log(`Intent Alignment Report \u2014 ${e}
77
+ `),console.log(` Intent starts: ${t.intentStartCount}`),console.log(` Intent ends: ${t.intentEndCount}`),console.log(` Intent checks: ${t.intentCheckCount}`),console.log(` Drift findings: ${t.intentDriftCount}`),t.averageAlignmentScore!==null&&console.log(` Avg alignment: ${t.averageAlignmentScore.toFixed(2)}`);const d=a.filter(c=>{const i=c.data;return i&&typeof i=="object"&&"aligned"in i&&!i.aligned}).slice(-5);if(d.length>0){console.log(`
78
+ Last ${d.length} misaligned action(s):`);for(const c of d){const i=c.data,l=typeof i.score=="number"?i.score.toFixed(2):"?",g=i.event,h=g?.action??"?",w=g?.primaryTarget??g?.targets?.[0]??g?.target??"?";console.log(` score: ${l} ${h} \u2192 ${w} (${c.timestamp.split("T")[0]})`)}}if(r.length>0){console.log(`
79
+ Recent drift findings: ${r.length}`);for(const c of r.slice(-3)){const i=c.data,l=i.severity??"?",g=i.description??"";console.log(` [${l}] ${typeof g=="string"?g.slice(0,80):g}`)}}await n.close()}async function yt(e,o){try{const n=await le({force:e,port:o});if(n.created.length>0){console.log("Created:");for(const t of n.created)console.log(` ${t}`)}if(n.skipped.length>0){console.log("Skipped (already exists \u2014 use --force to overwrite):");for(const t of n.skipped)console.log(` ${t}`)}if(n.merged.length>0){console.log("Merged:");for(const t of n.merged)console.log(` ${t}`)}if(n.errors.length>0){console.log("Errors:");for(const t of n.errors)console.log(` ${t}`)}n.errors.length===0&&console.log(`
80
+ Sentinel + Claude Code integration ready.`)}catch(n){console.error(`Init failed: ${n.message}`),process.exit(1)}}if(Fe){const e=y(f,"--port");await yt(xe,e?parseInt(e,10):void 0)}else if(Je&&p&&J&&Q)await pt(p,J,Q);else if(Qe&&p)await ht(p);else if(Ve&&p)await mt(p);else if(ze&&p)await wt(p);else if(ke)await se();else if(_)await ie(_);else if(He&&p)await gt(p);else if(Be&&p)await dt(p);else if(qe&&p)await ut(p);else if(Ke)await ft();else if(We&&p)await Ye(p,Y??"No reason provided");else if(Ue&&p)await Xe(p,Y??"No reason provided");else if(Ge&&p)await Ze(p);else if(_e)await et();else if(Ne)await st();else if(je)await it(p);else if(Me&&p&&G)await at(p,G,Re);else if(Ee&&p)await ot(p);else if(Le){const e=j?parseInt(j,10):30;await lt(e,X==="json"?"json":"markdown")}else if(Pe&&p){const e=j?parseInt(j,10):30;await rt(p,e,X==="json"?"json":"markdown")}else Oe?await ct():p?await nt(p):await tt();
@@ -0,0 +1,241 @@
1
+ import { v as Sentinel, e as AgentRole, S as SecurityFinding } from '../Sentinel-B_sv8Kiy.js';
2
+ import 'node:crypto';
3
+
4
+ /**
5
+ * Sentinel Gateway — HTTP server for Claude Code hook integration.
6
+ *
7
+ * Endpoints:
8
+ * POST /api/sentinel/pre-tool-use/:agentType
9
+ * POST /api/sentinel/post-tool-use/:agentType
10
+ * POST /api/sentinel/session-end/:agentType
11
+ * GET /api/sentinel/telemetry
12
+ * GET /api/sentinel/health
13
+ *
14
+ * Lifecycle:
15
+ * - Primary session end: SessionEnd hook → completeSession()
16
+ * - Fallback: SIGTERM/SIGINT → completeSession() → clean exit
17
+ *
18
+ * Follows patterns from webhookReceiver.ts:
19
+ * port 0 for tests, .once("error"), 1MB body limit.
20
+ */
21
+
22
+ interface SentinelGatewayOptions {
23
+ port?: number;
24
+ sentinel: Sentinel;
25
+ /** Agent ID used for sentinel.wrap() / sentinel.completeSession() calls. */
26
+ agentId: string;
27
+ /**
28
+ * Forbidden target patterns for Bash L1 path-token checking.
29
+ * Defaults to standard Sentinel forbidden patterns if not provided.
30
+ */
31
+ forbiddenPatterns?: string[];
32
+ /**
33
+ * Workspace-isolation gate (Approach B). When true, the gateway runs the
34
+ * per-workspace B-path: derive identity per request, lazily create a
35
+ * per-workspace runner with a merged role, route by the derived id, run the
36
+ * idle sweep. The daemon/CLI sets this ON by default (B5b-Phase-2 cutover);
37
+ * an operator can force the single-global-workspace escape hatch with
38
+ * SENTINEL_WORKSPACE_ISOLATION=0. The SentinelGateway constructor's own
39
+ * library-level default (this field unset, env unset) stays FALSE for
40
+ * programmatic callers/tests. With it OFF the daemon serves one global
41
+ * workspace; Approach A's foreign-workspace refusal has been retired, so a
42
+ * foreign request in that mode is served as global rather than refused.
43
+ */
44
+ workspaceIsolation?: boolean;
45
+ /**
46
+ * Operator ceiling role (the daemon's launch --policy role), merged with each
47
+ * workspace's narrowing yaml via B1. Required for the B-path; unused gate-off.
48
+ */
49
+ operatorCeiling?: AgentRole;
50
+ /** Home dir bounding workspace policy discovery (B-path only). Defaults to os.homedir(). */
51
+ home?: string;
52
+ }
53
+ declare class SentinelGateway {
54
+ private readonly configuredPort;
55
+ private readonly sentinel;
56
+ private readonly registry;
57
+ private readonly agentId;
58
+ private readonly telemetry;
59
+ private readonly forbiddenPatterns;
60
+ /** B5a gate (default off). True → per-workspace B-path; false → today's behavior. */
61
+ private readonly workspaceIsolation;
62
+ private readonly operatorCeiling;
63
+ private readonly home;
64
+ private server;
65
+ private running;
66
+ private signalHandlersInstalled;
67
+ private boundSigterm;
68
+ private boundSigint;
69
+ constructor(options: SentinelGatewayOptions);
70
+ get port(): number;
71
+ isRunning(): boolean;
72
+ start(): Promise<void>;
73
+ stop(): Promise<void>;
74
+ /**
75
+ * Session completion. Safe to call multiple times — idempotency is provided
76
+ * by classifier.flush() returning null when no events have accumulated
77
+ * (runner.ts completeSession L612-613). Supports multi-session gateway
78
+ * lifetimes: each cc session's events flush independently.
79
+ */
80
+ completeSessionSafe(routingId?: string): Promise<void>;
81
+ /**
82
+ * B5b-Phase-2: complete EVERY active session on shutdown, not just the global
83
+ * identity. Under workspace isolation each workspace runs its own runner with
84
+ * its own session + audit trail; a clean SIGTERM/SIGINT must flush each one,
85
+ * otherwise a workspace's tail events are lost on daemon shutdown. The global
86
+ * agentId is always included (it is the only runner gate-off, so this reduces
87
+ * to today's single completeSession in the escape-hatch case). Per-id failures
88
+ * are swallowed by completeSessionSafe so one bad runner can't strand the rest;
89
+ * completeSession is idempotent (flush() returns null with nothing queued).
90
+ */
91
+ completeAllSessionsSafe(): Promise<void>;
92
+ private installSignalHandlers;
93
+ private removeSignalHandlers;
94
+ private handleSignal;
95
+ private handleRequest;
96
+ /**
97
+ * Emit a workspace-mismatch refusal as a finding + telemetry. Originally
98
+ * Approach A's foreign-refuse emitter; after the B5b-Phase-2 cutover this is
99
+ * reused by the B-path (resolveBPathRouting) for the no-cwd / unresolvable-
100
+ * workspace FAIL-CLOSED case — a request whose originating workspace cannot be
101
+ * resolved is still refused (correct fail-closed), even though foreign-but-
102
+ * resolvable workspaces are now SERVED.
103
+ *
104
+ * The finding's type is `workspace_mismatch`, which is NOT in
105
+ * `ESCALATION_ELIGIBLE_TYPES` — so `getEffectiveBlockCount` / `maybeEscalate`
106
+ * never count it, by design. This is the honest mechanism: a workspace
107
+ * mismatch is a ROUTING error (request from a different workspace than this
108
+ * daemon serves), not the served agent's misbehavior, so it denies the action
109
+ * but does not move the served agent's escalation ladder. Escalation-
110
+ * ineligibility is a consequence of what the finding IS, not a borrowed
111
+ * ineligible type. (An earlier version typed this `unauthorized_target`, which
112
+ * IS escalation-eligible; the audit-log-derived count then pulled foreign-
113
+ * workspace refusals into the shared ladder and collaterally quarantined the
114
+ * legitimate session — the bug this fix closes.)
115
+ *
116
+ * Emitted via logFinding() (not handleGatewayDeny()), matching the gateway's
117
+ * other non-eligible findings — e.g. the L3 case C/D `bash_analysis` records,
118
+ * which also use logFinding(). handleGatewayDeny() is reserved for the
119
+ * escalation-eligible deny paths (L1/L2/etc.).
120
+ */
121
+ private logWorkspaceMismatch;
122
+ /**
123
+ * Approach B / B5a — resolve a request to its per-workspace routing identity.
124
+ * Returns the derived agentId (used for BOTH routing and the event label) after
125
+ * lazily ensuring the workspace's runner exists with its merged role. Returns
126
+ * null and sends a fail-closed refusal when the workspace is unresolvable
127
+ * (no/empty cwd) — reusing the workspace_mismatch finding type. Only called
128
+ * when the isolation gate is ON.
129
+ */
130
+ private resolveBPathRouting;
131
+ private handlePreToolUse;
132
+ private handlePostToolUse;
133
+ private handleSessionEnd;
134
+ /**
135
+ * UserPromptSubmit handler (Sprint 23 P1 — automatic per-prompt intent
136
+ * capture). Fires once per prompt, BEFORE the turn's first PreToolUse (cc
137
+ * blocks on hook completion), and the hook AWAITS this response — so by the
138
+ * time cc proceeds to the first tool call, any declared intent is already
139
+ * stored and the first action IS checked against it.
140
+ *
141
+ * The intent is declared ONLY when the prompt has a leading `INTENT:` line
142
+ * (the opt-in convention). With no `INTENT:` line we no-op (sticky): the
143
+ * prior active task is left as-is, so a multi-turn session declares once at
144
+ * the top and holds it; a later `INTENT:` line supersedes via the existing
145
+ * IntentTracker auto-end+replace. We do NOT feed the whole prompt as the
146
+ * description (keyword extraction would be noisy).
147
+ *
148
+ * Declaration only. This endpoint never blocks a prompt and emits no
149
+ * permission decision; it reuses the existing per-workspace routing and the
150
+ * existing Sentinel.startTask. No block-ladder interaction whatsoever.
151
+ */
152
+ private handleUserPromptSubmit;
153
+ private readBody;
154
+ private sendJson;
155
+ }
156
+
157
+ /**
158
+ * Gateway types for the Sentinel + Claude Code integration.
159
+ *
160
+ * Written from scratch — does NOT import from @anthropic/claude-code.
161
+ * Payload types derived from https://code.claude.com/docs/en/hooks (verified May 2026).
162
+ */
163
+
164
+ interface GatewayConfig {
165
+ port: number;
166
+ policyPath: string;
167
+ }
168
+ interface SessionEndInfo {
169
+ sessionId?: string;
170
+ cwd?: string;
171
+ reason?: string;
172
+ }
173
+ interface WrapResult {
174
+ blocked: boolean;
175
+ finding?: SecurityFinding;
176
+ }
177
+ type ToolDecision = "allowed" | "blocked";
178
+ /** Common fields present in all cc hook payloads. */
179
+ interface CcHookCommonFields {
180
+ session_id: string;
181
+ transcript_path?: string;
182
+ cwd: string;
183
+ permission_mode?: string;
184
+ hook_event_name?: string;
185
+ /** cc effort level — typed but ignored by Sprint 5 translator. */
186
+ effort?: {
187
+ level: "low" | "medium" | "high" | "xhigh" | "max";
188
+ };
189
+ /** Present for subagent tool calls — propagated to audit metadata as of Sprint 6c. */
190
+ agent_id?: string;
191
+ /** Present for subagent tool calls — propagated to audit metadata as of Sprint 6c. */
192
+ agent_type?: string;
193
+ }
194
+ /** PreToolUse hook input from Claude Code. */
195
+ interface CcPreToolUsePayload extends CcHookCommonFields {
196
+ tool_name: string;
197
+ tool_input: Record<string, unknown>;
198
+ tool_use_id?: string;
199
+ }
200
+ /** PostToolUse hook input from Claude Code. */
201
+ interface CcPostToolUsePayload extends CcHookCommonFields {
202
+ tool_name: string;
203
+ tool_input: Record<string, unknown>;
204
+ tool_use_id?: string;
205
+ tool_response?: unknown;
206
+ }
207
+ /** SessionStart hook input from Claude Code. */
208
+ type CcSessionStartPayload = CcHookCommonFields;
209
+ /** SessionEnd hook input from Claude Code. */
210
+ type CcSessionEndPayload = CcHookCommonFields;
211
+ /**
212
+ * Full permission decision enum from cc docs.
213
+ * Sprint 5 translator only emits "allow" and "deny".
214
+ * TODO: "ask" and "defer" are Sprint 6 product decisions
215
+ * (MEDIUM-finding triage UX).
216
+ */
217
+ type CcPermissionDecision = "allow" | "deny" | "ask" | "defer";
218
+ /** PreToolUse hook response to Claude Code. */
219
+ interface CcPreToolUseResponse {
220
+ hookSpecificOutput: {
221
+ hookEventName: "PreToolUse";
222
+ permissionDecision: CcPermissionDecision;
223
+ permissionDecisionReason?: string;
224
+ /** Input modification — Sprint 6 product decision. */
225
+ updatedInput?: Record<string, unknown>;
226
+ /** Additional context injected into Claude's context. */
227
+ additionalContext?: string;
228
+ };
229
+ }
230
+ /** PostToolUse hook response to Claude Code (no decision field). */
231
+ interface CcPostToolUseResponse {
232
+ hookSpecificOutput: {
233
+ hookEventName: "PostToolUse";
234
+ };
235
+ }
236
+ /** SessionEnd hook response to Claude Code (no decision field). */
237
+ interface CcSessionEndResponse {
238
+ hookSpecificOutput: Record<string, never>;
239
+ }
240
+
241
+ export { type CcHookCommonFields, type CcPermissionDecision, type CcPostToolUsePayload, type CcPostToolUseResponse, type CcPreToolUsePayload, type CcPreToolUseResponse, type CcSessionEndPayload, type CcSessionEndResponse, type CcSessionStartPayload, type GatewayConfig, SentinelGateway, type SentinelGatewayOptions, type SessionEndInfo, type ToolDecision, type WrapResult };
@@ -0,0 +1,10 @@
1
+ import {
2
+ SentinelGateway
3
+ } from "../chunk-Z3PWIJKT.js";
4
+ import "../chunk-FMZWHT4M.js";
5
+ import "../chunk-6MHWJATS.js";
6
+ import "../chunk-2FFMYSVC.js";
7
+ export {
8
+ SentinelGateway
9
+ };
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runGatewayDaemon
4
+ } from "./chunk-Z3PWIJKT.js";
5
+ import "./chunk-FMZWHT4M.js";
6
+ import "./chunk-6MHWJATS.js";
7
+ import "./chunk-2FFMYSVC.js";
8
+
9
+ // src/gatewayDaemon.ts
10
+ var args = process.argv.slice(2);
11
+ var policyPath;
12
+ var port;
13
+ for (let i = 0; i < args.length; i++) {
14
+ if (args[i] === "--policy" && args[i + 1]) policyPath = args[++i];
15
+ else if (args[i] === "--port" && args[i + 1]) port = parseInt(args[++i], 10);
16
+ }
17
+ if (!policyPath) {
18
+ console.error("[SENTINEL GATEWAY] --policy <path> is required");
19
+ process.exit(1);
20
+ }
21
+ runGatewayDaemon({ policyPath, port }).catch((err) => {
22
+ console.error("[SENTINEL GATEWAY] Fatal:", err);
23
+ process.exit(1);
24
+ });
25
+ //# sourceMappingURL=gatewayDaemon.js.map
@@ -0,0 +1,141 @@
1
+ import { A as AgentActivityEvent, S as SecurityFinding } from './Sentinel-B_sv8Kiy.js';
2
+ export { a as AcceptableAction, b as AdapterConfig, c as AgentBaseline, d as AgentMode, e as AgentRole, f as AlertChannel, g as AlertConfig, h as AllowResponse, i as AuditEntry, j as AuditQueryOptions, B as BlockResponse, C as CorrelationFinding, E as ExceptionApprovalContext, k as ExceptionApprovalFn, G as GuideResponse, H as HookCheckpoint, l as HookContext, m as HookHandler, n as HookRegistration, o as HookResponse, I as IntentAlignmentConfig, p as IntentAlignmentResult, M as ModifiableEventFields, q as MonitorOptions, O as OverlayDecisionType, R as RepoSensitivityMap, r as ReportOptions, s as RoleException, t as SecuritySeverity, u as SensitivityOverlay, v as Sentinel, w as SentinelConfig, T as TaskIntent } from './Sentinel-B_sv8Kiy.js';
3
+ import 'node:crypto';
4
+
5
+ interface SentinelPolicy {
6
+ version: "1.0";
7
+ agent: {
8
+ id: string;
9
+ name: string;
10
+ description?: string;
11
+ };
12
+ policy: {
13
+ allow: {
14
+ actions: string[];
15
+ targets: string[];
16
+ /**
17
+ * Host allowlist for network_request targets (Sprint 6b W3c).
18
+ * Entries are normalized on load: lowercased, trimmed, trailing dots stripped.
19
+ * Entries containing whitespace, slashes, or colons are rejected.
20
+ */
21
+ networkHosts?: string[];
22
+ };
23
+ forbid: {
24
+ targets: string[];
25
+ };
26
+ exceptions?: {
27
+ target: string;
28
+ allowedActions: string[];
29
+ requiresTask?: string;
30
+ requiresApproval?: boolean;
31
+ expiresAfter?: number;
32
+ downgradeKindTo?: "informational" | "actionable";
33
+ }[];
34
+ schedule?: {
35
+ hours: [number, number];
36
+ days?: string[];
37
+ };
38
+ limits?: {
39
+ maxEventsPerHour?: number;
40
+ maxSessionDuration?: number;
41
+ };
42
+ };
43
+ enforcement?: {
44
+ restrictAfter?: number;
45
+ quarantineAfter?: number;
46
+ approvalRequired?: boolean;
47
+ /** Minimum finding kind that triggers enforcement actions. Defaults to "actionable". */
48
+ minKind?: "informational" | "actionable";
49
+ /**
50
+ * List of finding types to promote from "informational" to "actionable".
51
+ * Cannot include LOCKED_ACTIONABLE_TYPES (they are already actionable).
52
+ */
53
+ promote?: string[];
54
+ /** Baseline maturity thresholds — overrides hardcoded defaults in DeviationDetector. */
55
+ baselineMaturity?: {
56
+ minSessions?: number;
57
+ minDaysObserved?: number;
58
+ minCategoryDiversity?: number;
59
+ };
60
+ };
61
+ alerts?: {
62
+ channels: (string | {
63
+ type: string;
64
+ minKind?: "informational" | "actionable";
65
+ })[];
66
+ webhookUrl?: string;
67
+ filePath?: string;
68
+ minSeverity?: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
69
+ /** Top-level minimum finding kind for alert routing. Defaults to "actionable". */
70
+ minKind?: "informational" | "actionable";
71
+ };
72
+ repo?: {
73
+ root?: string;
74
+ scanOnStartup?: boolean;
75
+ mapPath?: string;
76
+ overlayPath?: string;
77
+ };
78
+ }
79
+ declare function loadPolicy(yamlPath: string): Promise<SentinelPolicy>;
80
+ declare function loadPolicyFromString(yamlString: string): SentinelPolicy;
81
+
82
+ interface CliApprovalOptions {
83
+ timeoutMs?: number;
84
+ defaultOnTimeout?: boolean;
85
+ }
86
+ declare function createCliApproval(options?: CliApprovalOptions): (event: AgentActivityEvent, finding: SecurityFinding) => Promise<boolean>;
87
+
88
+ /**
89
+ * `sentinel init claude-code` — generates config files for cc + Sentinel integration.
90
+ *
91
+ * Creates:
92
+ * <cwd>/.sentinel.yaml — starter policy (skip if exists, --force overwrites)
93
+ * ~/.dahlia/fail-closed-tiers.json — tier config for hook script
94
+ * ~/.dahlia/cc-hook.mjs — hook script with gateway path substituted
95
+ * <cwd>/.claude/settings.local.json — always merged, never overwritten
96
+ *
97
+ * Pre-flight checks (all must pass before any files are written):
98
+ * 1. tsx is resolvable
99
+ * 2. Configured port (default 7847) is available
100
+ * 3. ~/.dahlia/ directory is creatable
101
+ */
102
+ interface InitReport {
103
+ created: string[];
104
+ skipped: string[];
105
+ merged: string[];
106
+ errors: string[];
107
+ }
108
+ declare function runInitClaudeCode(options: {
109
+ force?: boolean;
110
+ port?: number;
111
+ cwd?: string;
112
+ home?: string;
113
+ }): Promise<InitReport>;
114
+
115
+ /**
116
+ * Walks up from a given directory looking for .sentinel.yaml.
117
+ * Stops at $HOME (inclusive) or filesystem root.
118
+ */
119
+ declare function discoverPolicy(startDir: string, home: string): string | null;
120
+
121
+ /**
122
+ * `session-start` entry point — called by the hook script on SessionStart.
123
+ *
124
+ * 1. Discover .sentinel.yaml (walk up from cwd to $HOME)
125
+ * 2. Acquire gateway lock (PID file check)
126
+ * 3. If gateway already running → exit early
127
+ * 4. If no policy found → exit early (no gateway needed)
128
+ * 5. Spawn gateway detached, write PID file, unref
129
+ */
130
+ interface SessionStartResult {
131
+ action: "reused" | "spawned" | "no-policy";
132
+ pid?: number;
133
+ policyPath?: string;
134
+ }
135
+ declare function runSessionStart(options?: {
136
+ cwd?: string;
137
+ home?: string;
138
+ port?: number;
139
+ }): Promise<SessionStartResult>;
140
+
141
+ export { AgentActivityEvent, type CliApprovalOptions, type InitReport, SecurityFinding, type SentinelPolicy, type SessionStartResult, createCliApproval, discoverPolicy, loadPolicy, loadPolicyFromString, runInitClaudeCode, runSessionStart };
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ import {
2
+ runInitClaudeCode,
3
+ runSessionStart
4
+ } from "./chunk-3U3PKD4N.js";
5
+ import {
6
+ Sentinel,
7
+ createCliApproval
8
+ } from "./chunk-QFRDEISP.js";
9
+ import "./chunk-CUJKNIKT.js";
10
+ import {
11
+ discoverPolicy
12
+ } from "./chunk-FMZWHT4M.js";
13
+ import "./chunk-6MHWJATS.js";
14
+ import {
15
+ loadPolicy,
16
+ loadPolicyFromString
17
+ } from "./chunk-2FFMYSVC.js";
18
+ import "./chunk-NUXSUSYY.js";
19
+ export {
20
+ Sentinel,
21
+ createCliApproval,
22
+ discoverPolicy,
23
+ loadPolicy,
24
+ loadPolicyFromString,
25
+ runInitClaudeCode,
26
+ runSessionStart
27
+ };
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ import {
2
+ LogAdapter
3
+ } from "./chunk-PDWWRZXF.js";
4
+ export {
5
+ LogAdapter
6
+ };
7
+ //# sourceMappingURL=logAdapter-IB6ZDEV2.js.map