@@ -7,6 +7,7 @@ import os from 'os';
77import pm from 'picomatch' ;
88import { parse } from 'sh-syntax' ;
99import { askNativePopup , sendDesktopNotification } from './ui/native' ;
10+ import { computeRiskMetadata , RiskMetadata } from './context-sniper' ;
1011
1112// ── Feature file paths ────────────────────────────────────────────────────────
1213const PAUSED_FILE = path . join ( os . homedir ( ) , '.node9' , 'PAUSED' ) ;
@@ -639,6 +640,8 @@ export async function evaluatePolicy(
639640 reason ?: string ;
640641 matchedField ?: string ;
641642 matchedWord ?: string ;
643+ tier ?: 1 | 2 | 3 | 4 | 5 | 6 | 7 ;
644+ ruleName ?: string ;
642645} > {
643646 const config = getConfig ( ) ;
644647
@@ -656,6 +659,8 @@ export async function evaluatePolicy(
656659 decision : matchedRule . verdict ,
657660 blockedByLabel : `Smart Rule: ${ matchedRule . name ?? matchedRule . tool } ` ,
658661 reason : matchedRule . reason ,
662+ tier : 2 ,
663+ ruleName : matchedRule . name ?? matchedRule . tool ,
659664 } ;
660665 }
661666 }
@@ -675,7 +680,7 @@ export async function evaluatePolicy(
675680 // Inline arbitrary code execution is always a review
676681 const INLINE_EXEC_PATTERN = / ^ ( p y t h o n 3 ? | b a s h | s h | z s h | p e r l | r u b y | n o d e | p h p | l u a ) \s + ( - c | - e | - e v a l ) \s / i;
677682 if ( INLINE_EXEC_PATTERN . test ( shellCommand . trim ( ) ) ) {
678- return { decision : 'review' , blockedByLabel : 'Node9 Standard (Inline Execution)' } ;
683+ return { decision : 'review' , blockedByLabel : 'Node9 Standard (Inline Execution)' , tier : 3 } ;
679684 }
680685
681686 // Strip DML keywords from tokens so user dangerousWords like "delete"/"update"
@@ -714,7 +719,7 @@ export async function evaluatePolicy(
714719 if ( hasSystemDisaster || isRootWipe ) {
715720 // If it IS a system disaster, return review so the dev gets a
716721 // "Manual Nuclear Protection" popup as a final safety check.
717- return { decision : 'review' , blockedByLabel : 'Manual Nuclear Protection' } ;
722+ return { decision : 'review' , blockedByLabel : 'Manual Nuclear Protection' , tier : 3 } ;
718723 }
719724
720725 // For everything else (docker, psql, rmdir, delete, rm),
@@ -740,13 +745,15 @@ export async function evaluatePolicy(
740745 return {
741746 decision : 'review' ,
742747 blockedByLabel : `Project/Global Config — rule "${ rule . action } " (path blocked)` ,
748+ tier : 5 ,
743749 } ;
744750 const allAllowed = pathTokens . every ( ( p ) => matchesPattern ( p , rule . allowPaths || [ ] ) ) ;
745751 if ( allAllowed ) return { decision : 'allow' } ;
746752 }
747753 return {
748754 decision : 'review' ,
749755 blockedByLabel : `Project/Global Config — rule "${ rule . action } " (default block)` ,
756+ tier : 5 ,
750757 } ;
751758 }
752759 }
@@ -798,14 +805,15 @@ export async function evaluatePolicy(
798805 blockedByLabel : `Project/Global Config — dangerous word: "${ matchedDangerousWord } "` ,
799806 matchedWord : matchedDangerousWord ,
800807 matchedField,
808+ tier : 6 ,
801809 } ;
802810 }
803811
804812 // ── 7. Strict Mode Fallback ─────────────────────────────────────────────
805813 if ( config . settings . mode === 'strict' ) {
806814 const envConfig = getActiveEnvironment ( config ) ;
807815 if ( envConfig ?. requireApproval === false ) return { decision : 'allow' } ;
808- return { decision : 'review' , blockedByLabel : 'Global Config (Strict Mode Active)' } ;
816+ return { decision : 'review' , blockedByLabel : 'Global Config (Strict Mode Active)' , tier : 7 } ;
809817 }
810818
811819 return { decision : 'allow' } ;
@@ -1215,7 +1223,8 @@ async function askDaemon(
12151223 toolName : string ,
12161224 args : unknown ,
12171225 meta ?: { agent ?: string ; mcpServer ?: string } ,
1218- signal ?: AbortSignal // NEW: Added signal
1226+ signal ?: AbortSignal ,
1227+ riskMetadata ?: RiskMetadata
12191228) : Promise < 'allow' | 'deny' | 'abandoned' > {
12201229 const base = `http://${ DAEMON_HOST } :${ DAEMON_PORT } ` ;
12211230
@@ -1229,7 +1238,7 @@ async function askDaemon(
12291238 const checkRes = await fetch ( `${ base } /check` , {
12301239 method : 'POST' ,
12311240 headers : { 'Content-Type' : 'application/json' } ,
1232- body : JSON . stringify ( { toolName, args, agent : meta ?. agent , mcpServer : meta ?. mcpServer } ) ,
1241+ body : JSON . stringify ( { toolName, args, agent : meta ?. agent , mcpServer : meta ?. mcpServer , ... ( riskMetadata && { riskMetadata } ) } ) ,
12331242 signal : checkCtrl . signal ,
12341243 } ) ;
12351244 if ( ! checkRes . ok ) throw new Error ( 'Daemon fail' ) ;
@@ -1261,7 +1270,8 @@ async function askDaemon(
12611270async function notifyDaemonViewer (
12621271 toolName : string ,
12631272 args : unknown ,
1264- meta ?: { agent ?: string ; mcpServer ?: string }
1273+ meta ?: { agent ?: string ; mcpServer ?: string } ,
1274+ riskMetadata ?: RiskMetadata
12651275) : Promise < string > {
12661276 const base = `http://${ DAEMON_HOST } :${ DAEMON_PORT } ` ;
12671277 const res = await fetch ( `${ base } /check` , {
@@ -1273,6 +1283,7 @@ async function notifyDaemonViewer(
12731283 slackDelegated : true ,
12741284 agent : meta ?. agent ,
12751285 mcpServer : meta ?. mcpServer ,
1286+ ...( riskMetadata && { riskMetadata } ) ,
12761287 } ) ,
12771288 signal : AbortSignal . timeout ( 3000 ) ,
12781289 } ) ;
@@ -1381,6 +1392,7 @@ export async function authorizeHeadless(
13811392 let explainableLabel = 'Local Config' ;
13821393 let policyMatchedField : string | undefined ;
13831394 let policyMatchedWord : string | undefined ;
1395+ let riskMetadata : RiskMetadata | undefined ;
13841396
13851397 if ( config . settings . mode === 'audit' ) {
13861398 if ( ! isIgnoredTool ( toolName ) ) {
@@ -1424,6 +1436,14 @@ export async function authorizeHeadless(
14241436 explainableLabel = policyResult . blockedByLabel || 'Local Config' ;
14251437 policyMatchedField = policyResult . matchedField ;
14261438 policyMatchedWord = policyResult . matchedWord ;
1439+ riskMetadata = computeRiskMetadata (
1440+ args ,
1441+ policyResult . tier ?? 6 ,
1442+ explainableLabel ,
1443+ policyMatchedField ,
1444+ policyMatchedWord ,
1445+ policyResult . ruleName ,
1446+ ) ;
14271447
14281448 const persistent = getPersistentDecision ( toolName ) ;
14291449 if ( persistent === 'allow' ) {
@@ -1453,7 +1473,7 @@ export async function authorizeHeadless(
14531473
14541474 if ( cloudEnforced ) {
14551475 try {
1456- const initResult = await initNode9SaaS ( toolName , args , creds ! , meta ) ;
1476+ const initResult = await initNode9SaaS ( toolName , args , creds ! , meta , riskMetadata ) ;
14571477
14581478 if ( ! initResult . pending ) {
14591479 return {
@@ -1544,7 +1564,7 @@ export async function authorizeHeadless(
15441564 ( async ( ) => {
15451565 try {
15461566 if ( isDaemonRunning ( ) && internalToken && ! options ?. calledFromDaemon ) {
1547- viewerId = await notifyDaemonViewer ( toolName , args , meta ) . catch ( ( ) => null ) ;
1567+ viewerId = await notifyDaemonViewer ( toolName , args , meta , riskMetadata ) . catch ( ( ) => null ) ;
15481568 }
15491569 const cloudResult = await pollNode9SaaS ( cloudRequestId , creds ! , signal ) ;
15501570
@@ -1613,7 +1633,7 @@ export async function authorizeHeadless(
16131633 console . error ( chalk . cyan ( ` URL → http://${ DAEMON_HOST } :${ DAEMON_PORT } /\n` ) ) ;
16141634 }
16151635
1616- const daemonDecision = await askDaemon ( toolName , args , meta , signal ) ;
1636+ const daemonDecision = await askDaemon ( toolName , args , meta , signal , riskMetadata ) ;
16171637 if ( daemonDecision === 'abandoned' ) throw new Error ( 'Abandoned' ) ;
16181638
16191639 const isApproved = daemonDecision === 'allow' ;
@@ -1966,7 +1986,8 @@ async function initNode9SaaS(
19661986 toolName : string ,
19671987 args : unknown ,
19681988 creds : { apiKey : string ; apiUrl : string } ,
1969- meta ?: { agent ?: string ; mcpServer ?: string }
1989+ meta ?: { agent ?: string ; mcpServer ?: string } ,
1990+ riskMetadata ?: RiskMetadata
19701991) : Promise < {
19711992 pending : boolean ;
19721993 requestId ?: string ;
@@ -1991,6 +2012,7 @@ async function initNode9SaaS(
19912012 cwd : process . cwd ( ) ,
19922013 platform : os . platform ( ) ,
19932014 } ,
2015+ ...( riskMetadata && { riskMetadata } ) ,
19942016 } ) ,
19952017 signal : controller . signal ,
19962018 } ) ;
0 commit comments