3535from iclaw .exec_tool import exec_command as exec
3636from iclaw .github_api import UnsupportedModelError , chat , get_copilot_token
3737from iclaw .providers import openrouter
38+ from iclaw .safety import check_edit_safety , check_exec_safety
3839from iclaw .tools .browser_tool import dispatch_browser_call
3940from iclaw .tools .defs import TOOLS
4041from iclaw .tools .edit_tool import EditTool
6768 ("/copy" , "Copy last Copilot response to clipboard" ),
6869 ("/read" , "Print file contents to terminal (usage: /read <path>)" ),
6970 ("/cd" , "Change directory and restart iclaw (usage: /cd <path>)" ),
71+ ("/safety" , "Set safety level (usage: /safety [low|high])" ),
7072 ("/clear" , "Clear conversation history" ),
7173 ("/compact" , "Compact conversation history using LLM" ),
7274 ("/export" , "Export full conversation history to JSON file" ),
@@ -98,6 +100,7 @@ async def _main():
98100 proxy = settings ["proxy" ]
99101 ca_bundle = settings ["ca_bundle" ]
100102 log_level = settings ["log_level" ]
103+ safety_level = settings ["safety_level" ]
101104 log .set_level (
102105 {"info" : log .INFO , "verbose" : log .VERBOSE }.get (log_level , log .VERBOSE )
103106 )
@@ -186,6 +189,7 @@ async def _main():
186189 proxy = proxy ,
187190 ca_bundle = ca_bundle ,
188191 log_level = log_level ,
192+ safety_level = safety_level ,
189193 )
190194 continue
191195 if user_input == "/model" :
@@ -199,6 +203,7 @@ async def _main():
199203 proxy = proxy ,
200204 ca_bundle = ca_bundle ,
201205 log_level = log_level ,
206+ safety_level = safety_level ,
202207 )
203208 continue
204209 if user_input .startswith ("/search" ) or user_input == "/search" :
@@ -267,6 +272,7 @@ async def _main():
267272 proxy = proxy ,
268273 ca_bundle = ca_bundle ,
269274 log_level = log_level ,
275+ safety_level = safety_level ,
270276 )
271277 continue
272278 if user_input == "/proxy" or user_input .startswith ("/proxy " ):
@@ -281,6 +287,7 @@ async def _main():
281287 proxy = proxy ,
282288 ca_bundle = ca_bundle ,
283289 log_level = log_level ,
290+ safety_level = safety_level ,
284291 )
285292 continue
286293 if user_input == "/ca_bundle" or user_input .startswith ("/ca_bundle " ):
@@ -295,6 +302,7 @@ async def _main():
295302 proxy = proxy ,
296303 ca_bundle = ca_bundle ,
297304 log_level = log_level ,
305+ safety_level = safety_level ,
298306 )
299307 continue
300308 if user_input == "/log" or user_input .startswith ("/log " ):
@@ -310,6 +318,7 @@ async def _main():
310318 proxy = proxy ,
311319 ca_bundle = ca_bundle ,
312320 log_level = log_level ,
321+ safety_level = safety_level ,
313322 )
314323 continue
315324 if user_input == "/status" :
@@ -319,9 +328,29 @@ async def _main():
319328 print (f" proxy: { proxy or '(not set)' } " )
320329 print (f" ca_bundle: { ca_bundle or '(system default)' } " )
321330 print (f" log_level: { log_level } " )
331+ print (f" safety_level: { safety_level } " )
322332 print (f" cwd: { os .getcwd ()} " )
323333 print ()
324334 continue
335+ if user_input == "/safety" or user_input .startswith ("/safety " ):
336+ parts = user_input .split (maxsplit = 1 )
337+ if len (parts ) > 1 and parts [1 ].strip () in ("low" , "high" ):
338+ safety_level = parts [1 ].strip ()
339+ save_session_settings (
340+ model_provider = model_provider ,
341+ current_model = current_model ,
342+ search_provider = search_provider ,
343+ proxy = proxy ,
344+ ca_bundle = ca_bundle ,
345+ log_level = log_level ,
346+ safety_level = safety_level ,
347+ )
348+ print (f"Safety level set to: { safety_level } " )
349+ else :
350+ print (f"Current safety level: { safety_level } " )
351+ print ("Usage: /safety low — no restrictions" )
352+ print (" /safety high — file mutations restricted to CWD only" )
353+ continue
325354 if user_input == "/cd" or user_input .startswith ("/cd " ):
326355 parts = user_input .split (maxsplit = 1 )
327356 target = (
@@ -360,8 +389,13 @@ async def _main():
360389 if len (parts ) < 2 or not parts [1 ].strip ():
361390 print ("Usage: /cmd <command>" , file = sys .stderr )
362391 else :
363- output = exec (parts [1 ])
364- print (output )
392+ cmd = parts [1 ]
393+ block_reason = check_exec_safety (cmd , safety_level )
394+ if block_reason :
395+ print (block_reason , file = sys .stderr )
396+ else :
397+ output = exec (cmd )
398+ print (output )
365399 continue
366400 if user_input == "/browse" or user_input .startswith ("/browse " ):
367401 parts = user_input .split (maxsplit = 1 )
@@ -491,7 +525,13 @@ async def _main():
491525 log .log_verbose (f"[tool] Result: ({ len (search_context )} chars)" )
492526
493527 if function_name == "exec" :
494- output = exec (function_args .get ("command" ))
528+ cmd = function_args .get ("command" )
529+ block_reason = check_exec_safety (cmd , safety_level )
530+ if block_reason :
531+ output = block_reason
532+ log .log_verbose (f"[safety] Blocked exec: { block_reason } " )
533+ else :
534+ output = exec (cmd )
495535 tool_logs .append (
496536 {
497537 "timestamp" : time .time (),
@@ -514,11 +554,16 @@ async def _main():
514554
515555 if function_name == "edit" :
516556 file_path = function_args .get ("file_path" )
517- result = EditTool .edit (
518- file_path , function_args .get ("edit_content" )
519- )
520- with open (file_path , "w" ) as f :
521- f .write (result )
557+ block_reason = check_edit_safety (file_path , safety_level )
558+ if block_reason :
559+ result = block_reason
560+ log .log_verbose (f"[safety] Blocked edit: { block_reason } " )
561+ else :
562+ result = EditTool .edit (
563+ file_path , function_args .get ("edit_content" )
564+ )
565+ with open (file_path , "w" ) as f :
566+ f .write (result )
522567 tool_logs .append (
523568 {
524569 "timestamp" : time .time (),
0 commit comments