Skip to content

Commit 2878209

Browse files
metiu1claude
andcommitted
v0.3.56: Ask-mode confirms run_code/media, grep/glob skip binary noise
- Ask mode now confirms every code-running / state-changing tool, not just file writes: a central approval gate covers run_code, media generation, install_model, rename_file and create_document (blocked in Plan mode). This fixes the agent running code without asking in Ask mode. - grep_search / glob_search now skip .git, __pycache__, node_modules and binary files (.pyc, images, archives…). The model was looping on binary .pyc garbage matches until it ran out of rounds and returned nothing. - Terminal approval (y/n/a) reads in raw mode like ask_user, so confirming works reliably mid tool-loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dc13f4a commit 2878209

8 files changed

Lines changed: 156 additions & 10 deletions

File tree

vortelio-pip/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "vortelio"
7-
version = "0.3.55"
7+
version = "0.3.56"
88
description = "Local-first AI platform. Run LLMs, generate images & video, transcribe audio, create 3D — on your own machine. OpenAI & Ollama API compatible. Apache 2.0."
99
readme = "README.md"
1010
requires-python = ">=3.8"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.55"
1+
__version__ = "0.3.56"

vortelio-pip/src/vortelio_cli/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import urllib.request
1111
from pathlib import Path
1212

13-
VERSION = "0.3.55"
13+
VERSION = "0.3.56"
1414
RELEASE_BASE = os.environ.get(
1515
"VORTELIO_RELEASE_BASE",
1616
f"https://github.com/metiu1/Vortelio/releases/download/v{VERSION}",
9.5 KB
Binary file not shown.

vortelio/ciao.pdf

616 Bytes
Binary file not shown.

vortelio/internal/cli/commands/code.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package commands
22

33
import (
4-
"bufio"
54
"encoding/json"
65
"fmt"
76
"io/fs"
@@ -165,8 +164,7 @@ func (s *codeSession) approve(tool, summary, args string) bool {
165164
fmt.Printf("\n %s⚠ Conferma azione%s %s%s%s\n", cYell, cReset, cBold, summary, cReset)
166165
fmt.Printf(" %s%s%s\n", cDim, truncStr(args, 200), cReset)
167166
fmt.Printf(" [%sy%s] sì [%sn%s] no [%sa%s] sì a tutto (auto) ", cGreen, cReset, cRed, cReset, cCyan, cReset)
168-
r := bufio.NewReader(os.Stdin)
169-
in, _ := r.ReadString('\n')
167+
in := promptLineRaw("")
170168
switch strings.ToLower(strings.TrimSpace(in)) {
171169
case "y", "yes", "s", "si", "":
172170
return true

vortelio/internal/server/server_agentic.go

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

vortelio/internal/version/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ package version
33
// Version è impostabile a build time via:
44
//
55
// -ldflags "-X github.com/vortelio/vortelio/internal/version.Version=X.Y.Z"
6-
var Version = "0.3.55"
6+
var Version = "0.3.56"

0 commit comments

Comments
 (0)