@@ -48,6 +48,25 @@ function matchesGlob(pattern: string, str: string): boolean {
4848 return new RegExp ( `^${ regex } $` ) . test ( norm ) ;
4949}
5050
51+ /**
52+ * Extract the primary arg value for a tool call, used for granular session allow matching.
53+ * Returns the raw string value (not resolved) so globs can match commands and URLs too.
54+ */
55+ function extractPrimaryArg ( moduleName : string , params : Record < string , unknown > ) : string | null {
56+ if ( moduleName === 'Browser' || moduleName === 'Network' ) {
57+ const v = params . url ?? params . src ?? null ;
58+ return v != null ? String ( v ) : null ;
59+ }
60+ if ( moduleName === 'Shell' ) {
61+ const v = params . command ?? params . cmd ?? null ;
62+ return v != null ? String ( v ) : null ;
63+ }
64+ const raw = params . path ?? params . file_path ?? params . filePath ?? params . target ?? null ;
65+ if ( raw != null ) return path . resolve ( expandTilde ( String ( raw ) ) ) ;
66+ const first = Object . values ( params ) [ 0 ] ;
67+ return first != null ? String ( first ) : null ;
68+ }
69+
5170/**
5271 * Extract the path or URL that a tool call is targeting, for path-policy evaluation.
5372 * Returns null if no target can be determined (policy check is skipped).
@@ -124,6 +143,15 @@ export class Interceptor {
124143 this . logEnabled = logEnabled ;
125144 }
126145
146+ /** Hot-reload the policy without restarting the gateway. */
147+ reloadPolicy ( policy : SecurityPolicy ) : void {
148+ this . policy = policy ;
149+ logger . info ( 'Interceptor: policy hot-reloaded' , {
150+ modules : Object . keys ( policy . modules ) ,
151+ defaultAction : policy . defaultAction ,
152+ } ) ;
153+ }
154+
127155 /**
128156 * Evaluate the security policy for a tool call.
129157 */
@@ -134,7 +162,7 @@ export class Interceptor {
134162 sessionKey ?: string ,
135163 intervention ?: InterventionMetadata
136164 ) : Promise < void > {
137- const baseRule = this . lookupRule ( moduleName , methodName ) ;
165+ const baseRule = this . lookupRule ( moduleName , methodName , sessionKey , args ) ;
138166 // Path-policy check runs before intervention so a denied path is never
139167 // upgradeable to ASK — it's a hard DENY.
140168 const originalRule = applyPathPolicy ( baseRule , moduleName , args ) ;
@@ -177,14 +205,13 @@ export class Interceptor {
177205 ) ;
178206
179207 throw new Error (
180- `[ClawReins:APPROVAL_REQUIRED] ${ moduleName } .${ methodName } () is blocked pending human approval. ` +
181- `Risk: ${ detail } \n` +
208+ `Action not permitted: ${ moduleName } .${ methodName } () requires human authorization.\n` +
182209 instructions
183210 ) ;
184211 }
185212
186213 throw new Error (
187- `ClawReins Security Violation : ${ moduleName } .${ methodName } () was DENIED . ${ detail } `
214+ `Action not permitted : ${ moduleName } .${ methodName } () was denied . ${ detail } `
188215 ) ;
189216 }
190217 }
@@ -276,26 +303,41 @@ export class Interceptor {
276303 if ( strict ) {
277304 return (
278305 `${ screenshotInstruction } ` +
279- `⚠️ HIGH-RISK action requires explicit human confirmation.\n` +
280306 `Action: ${ summary } \n` +
281- `An out-of-band approval notification has been sent to the human.\n` +
282- `WAIT — do NOT retry this tool. Do NOT attempt to self-approve.`
307+ `WAIT — do NOT retry this tool. Do NOT attempt to proceed without explicit authorization.`
283308 ) ;
284309 }
285310
286311 return (
287312 `${ screenshotInstruction } ` +
288313 `Action: ${ summary } \n` +
289- `An approval request has been sent to the human out-of-band.\n` +
290- `WAIT for their response before retrying. Do NOT attempt to self-approve.`
314+ `WAIT for authorization before retrying. Do NOT attempt to proceed independently.`
291315 ) ;
292316 }
293317
294- private lookupRule ( moduleName : string , methodName : string ) : SecurityRule {
318+ private lookupRule ( moduleName : string , methodName : string , sessionKey ?: string , args ?: unknown [ ] ) : SecurityRule {
295319 const moduleRules = this . policy . modules [ moduleName ] ;
320+ const rule = moduleRules ?. [ methodName ] ;
321+
322+ if ( rule ) {
323+ // Blanket session allow — matches all calls to this module.method.
324+ if ( sessionKey && rule . allowForSessionKeys ?. some ( prefix => sessionKey . startsWith ( prefix ) ) ) {
325+ return { action : 'ALLOW' , description : `Allowed for session ${ sessionKey } ` } ;
326+ }
296327
297- if ( moduleRules && moduleRules [ methodName ] ) {
298- return moduleRules [ methodName ] ;
328+ // Granular session+arg allow — matches by session prefix AND primary arg glob.
329+ if ( sessionKey && args && rule . allowForSessionGranular ?. length ) {
330+ const params = ( args [ 0 ] as Record < string , unknown > ) ?? { } ;
331+ const primaryArg = extractPrimaryArg ( moduleName , params ) ;
332+ if ( primaryArg !== null ) {
333+ const matched = rule . allowForSessionGranular . some (
334+ entry => sessionKey . startsWith ( entry . sessionKeyPrefix ) && matchesGlob ( entry . argGlob , primaryArg )
335+ ) ;
336+ if ( matched ) return { action : 'ALLOW' , description : `Allowed for session ${ sessionKey } with arg pattern` } ;
337+ }
338+ }
339+
340+ return rule ;
299341 }
300342
301343 return {
0 commit comments