@@ -453,7 +453,134 @@ func buildAgenticProvider(cfg *AgenticConfig, emit rt.ToolEventEmitter) rt.ToolP
453453 if len (providers ) > 0 {
454454 providers = append (providers , & selfProvider {emit : emit , ask : cfg .AskFunc })
455455 }
456- return rt .NewCompositeProvider (providers ... )
456+ mode := cfg .Mode
457+ if mode == "" {
458+ mode = "ask"
459+ }
460+ // Centralized approval gate: in Ask mode every state-changing/code-running
461+ // tool (run_code, media generation, install, rename, create_document) must be
462+ // confirmed; in Plan mode they are blocked. (write_file/edit_file/run_shell
463+ // are gated inside the coding provider itself, so they are not listed here to
464+ // avoid a double prompt.)
465+ return & gatedProvider {
466+ inner : rt .NewCompositeProvider (providers ... ),
467+ mode : mode ,
468+ approve : cfg .ApproveFunc ,
469+ emit : emit ,
470+ }
471+ }
472+
473+ // gatedRiskyTools are tools that act on the system or run code and therefore need
474+ // confirmation in Ask mode (and are blocked in Plan mode).
475+ var gatedRiskyTools = map [string ]bool {
476+ "run_code" : true ,
477+ "create_document" : true ,
478+ "generate_image" : true ,
479+ "generate_video" : true ,
480+ "text_to_speech" : true ,
481+ "generate_3d" : true ,
482+ "install_model" : true ,
483+ "rename_file" : true ,
484+ }
485+
486+ // gatedProvider wraps the full tool set and enforces Ask/Plan-mode approval for
487+ // risky tools that the per-tool providers do not gate themselves.
488+ type gatedProvider struct {
489+ inner rt.ToolProvider
490+ mode string
491+ approve func (tool , summary , args string ) bool
492+ emit rt.ToolEventEmitter
493+ mu sync.Mutex
494+ counter int
495+ }
496+
497+ func (g * gatedProvider ) Tools () []rt.ToolDef { return g .inner .Tools () }
498+
499+ func (g * gatedProvider ) Execute (name , args string ) (string , error ) {
500+ if gatedRiskyTools [name ] {
501+ switch g .mode {
502+ case "plan" :
503+ return "" , fmt .Errorf ("blocked: in Plan mode the agent cannot run code or generate/modify files. Switch to Ask or Auto to proceed" )
504+ case "auto" :
505+ // proceed without prompting
506+ default : // ask
507+ if ! g .requestApproval (name , gatedSummary (name , args ), args ) {
508+ return "" , fmt .Errorf ("denied by user" )
509+ }
510+ }
511+ }
512+ return g .inner .Execute (name , args )
513+ }
514+
515+ func gatedSummary (name , args string ) string {
516+ var m map [string ]interface {}
517+ json .Unmarshal ([]byte (args ), & m )
518+ clip := func (s string , n int ) string {
519+ s = strings .ReplaceAll (s , "\n " , " " )
520+ if len (s ) > n {
521+ return s [:n ] + "…"
522+ }
523+ return s
524+ }
525+ switch name {
526+ case "run_code" :
527+ return "Esegui codice: " + clip (fmt .Sprint (m ["code" ]), 120 )
528+ case "create_document" :
529+ return fmt .Sprintf ("Crea documento: %v" , m ["path" ])
530+ case "install_model" :
531+ return fmt .Sprintf ("Installa modello: %v" , m ["model" ])
532+ case "rename_file" :
533+ return fmt .Sprintf ("Rinomina file: %v" , m ["path" ])
534+ case "generate_image" , "generate_video" , "generate_3d" , "text_to_speech" :
535+ return fmt .Sprintf ("%s: %v" , name , clip (fmt .Sprint (m ["prompt" ]), 100 ))
536+ }
537+ return name
538+ }
539+
540+ func (g * gatedProvider ) requestApproval (tool , summary , argsJSON string ) bool {
541+ if g .approve != nil { // CLI synchronous y/n
542+ return g .approve (tool , summary , argsJSON )
543+ }
544+ g .mu .Lock ()
545+ g .counter ++
546+ id := fmt .Sprintf ("gappr_%d_%d" , time .Now ().UnixNano (), g .counter )
547+ g .mu .Unlock ()
548+ ch := registerApproval (id )
549+ if g .emit != nil {
550+ g .emit ("approval_request" , map [string ]interface {}{
551+ "id" : id , "tool" : tool , "summary" : summary , "arguments" : json .RawMessage (argsJSON ),
552+ })
553+ }
554+ select {
555+ case ok := <- ch :
556+ return ok
557+ case <- time .After (5 * time .Minute ):
558+ resolveApproval (id , false )
559+ return false
560+ }
561+ }
562+
563+ // isNoiseDir reports directories whose contents are build artefacts or VCS state
564+ // and should be skipped by grep/glob so the model isn't fed binary garbage.
565+ func isNoiseDir (name string ) bool {
566+ switch name {
567+ case ".git" , "__pycache__" , "node_modules" , ".venv" , "venv" , "dist" , "build" , ".idea" , ".vscode" , ".mypy_cache" , ".pytest_cache" , ".next" , "target" :
568+ return true
569+ }
570+ return false
571+ }
572+
573+ // isBinaryName reports paths with a non-text extension that grep/glob should skip.
574+ func isBinaryName (path string ) bool {
575+ switch strings .ToLower (filepath .Ext (path )) {
576+ case ".pyc" , ".pyo" , ".exe" , ".dll" , ".so" , ".dylib" , ".o" , ".a" , ".class" , ".jar" ,
577+ ".zip" , ".gz" , ".tar" , ".7z" , ".rar" , ".bin" , ".db" , ".sqlite" , ".sqlite3" ,
578+ ".png" , ".jpg" , ".jpeg" , ".gif" , ".webp" , ".bmp" , ".ico" , ".tiff" ,
579+ ".mp4" , ".mov" , ".avi" , ".mkv" , ".webm" , ".mp3" , ".wav" , ".flac" , ".ogg" ,
580+ ".pdf" , ".woff" , ".woff2" , ".ttf" , ".otf" , ".eot" :
581+ return true
582+ }
583+ return false
457584}
458585
459586// selfProvider lets the agent create reusable skills and ask the user questions
@@ -800,7 +927,16 @@ func (c *codingProvider) glob(argsJSON string) (string, error) {
800927 }
801928 var matches []string
802929 filepath .WalkDir (base , func (path string , d os.DirEntry , err error ) error {
803- if err != nil || d .IsDir () {
930+ if err != nil {
931+ return nil
932+ }
933+ if d .IsDir () {
934+ if isNoiseDir (d .Name ()) {
935+ return filepath .SkipDir
936+ }
937+ return nil
938+ }
939+ if isBinaryName (path ) {
804940 return nil
805941 }
806942 rel , _ := filepath .Rel (base , path )
@@ -849,7 +985,16 @@ func (c *codingProvider) grep(argsJSON string) (string, error) {
849985 }
850986 var hits []string
851987 filepath .WalkDir (base , func (path string , d os.DirEntry , err error ) error {
852- if err != nil || d .IsDir () {
988+ if err != nil {
989+ return nil
990+ }
991+ if d .IsDir () {
992+ if isNoiseDir (d .Name ()) {
993+ return filepath .SkipDir
994+ }
995+ return nil
996+ }
997+ if isBinaryName (path ) {
853998 return nil
854999 }
8551000 info , _ := d .Info ()
@@ -860,6 +1005,9 @@ func (c *codingProvider) grep(argsJSON string) (string, error) {
8601005 if e != nil {
8611006 return nil
8621007 }
1008+ if strings .IndexByte (string (data ), 0 ) >= 0 {
1009+ return nil // binary file (null byte) — skip garbage matches
1010+ }
8631011 for i , line := range strings .Split (string (data ), "\n " ) {
8641012 if strings .Contains (line , a .Query ) {
8651013 rel , _ := filepath .Rel (base , path )
0 commit comments