@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/LICENSE +201 -0
- package/README.md +96 -0
- package/dist/Sentinel-B_sv8Kiy.d.ts +1785 -0
- package/dist/Sentinel-JLQL3YRD.js +10 -0
- package/dist/auditTrailKeys-GKCW5KUD.js +23 -0
- package/dist/chunk-2FFMYSVC.js +428 -0
- package/dist/chunk-3U3PKD4N.js +539 -0
- package/dist/chunk-6MHWJATS.js +1221 -0
- package/dist/chunk-CUJKNIKT.js +62 -0
- package/dist/chunk-FMZWHT4M.js +20 -0
- package/dist/chunk-NUXSUSYY.js +95 -0
- package/dist/chunk-PDWWRZXF.js +238 -0
- package/dist/chunk-QFRDEISP.js +7429 -0
- package/dist/chunk-Z3PWIJKT.js +2268 -0
- package/dist/cli.js +80 -0
- package/dist/gateway/index.d.ts +241 -0
- package/dist/gateway/index.js +10 -0
- package/dist/gatewayDaemon.js +25 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +28 -0
- package/dist/logAdapter-IB6ZDEV2.js +7 -0
- package/dist/mcpAdapter-R47GX2P3.js +178 -0
- package/dist/pidManager-ZYC7SICM.js +15 -0
- package/dist/policyLoader-6KR5VFVV.js +15 -0
- package/dist/webhookReceiver-NAVMQ6N5.js +203 -0
- package/package.json +61 -0
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,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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|