diff --git a/README.md b/README.md index e2f3dde..fae7c43 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ zot --help | `--no-ext` | Skip extension discovery for this run. `--ext` still works on top, so `--no-ext --ext ./x` runs only `x`. | | `--with-skills` | Also load user-installed skills. Without this, only the built-in skills shipped in the binary are loaded. | | `--no-skill` | Disable all skills, including built-ins. No `skill` tool is registered and the system prompt has no skill manifest. | -| `--no-yolo` | Confirm every tool call before it runs. In the TUI, a dialog shows the tool name and a one-line preview of its args with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. In print / json / rpc modes (no interactive prompt) every tool call is auto-refused with a reason the model can learn from. Type `/yolo` in the TUI to disable the gate for the rest of the session. | +| `--no-yolo` | Confirm every tool call before it runs (interactive TUI only). A dialog shows the tool name and a one-line preview of its args with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. Ignored with a stderr warning in print / json / rpc modes, where tools still run freely so scripts and automation keep working. Type `/yolo` in the TUI to disable the gate for the rest of the session. | ## Tools diff --git a/internal/agent/args.go b/internal/agent/args.go index d45c748..d1b4f54 100644 --- a/internal/agent/args.go +++ b/internal/agent/args.go @@ -69,9 +69,12 @@ type Args struct { // and waits for an explicit yes/no. The user can also pick // "always for this tool this session" or "always for anything // this session" to stop being prompted again. Defaults off - // (yolo mode): tools run without asking. In -p / --json / rpc - // modes there's no interactive TUI, so --no-yolo auto-refuses - // every tool call with a reason the model can learn from. + // (yolo mode): tools run without asking. + // + // No effect in -p / --json / rpc modes, which have no + // interactive prompt. A warning is printed to stderr on startup + // so scripts know the flag is ignored, but tools still run + // freely so automated workflows keep working. NoYolo bool ListModels bool @@ -287,8 +290,8 @@ flags: default: only built-in skills load --no-yolo ask before running every tool call - (interactive mode only; in -p / --json - / rpc this refuses every tool call) + (interactive tui only; ignored with + a stderr warning in -p / --json / rpc) --max-steps N agent loop iteration cap (default 50) --list-models print known models and exit diff --git a/internal/agent/cli.go b/internal/agent/cli.go index 81d271e..a0dd932 100644 --- a/internal/agent/cli.go +++ b/internal/agent/cli.go @@ -184,12 +184,14 @@ func Run(rawArgs []string, version string) error { // ---- print / json modes: require credentials, run single-shot ---- func runPrintMode(ctx context.Context, args Args, version string) error { + if args.NoYolo { + fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in print mode (no interactive prompt available); tools will run without confirmation") + } r, err := Resolve(args, true) if err != nil { return err } ag := r.NewAgent() - wireNoYoloAutoRefuse(ag, args) sess, _ := openOrCreateSession(args, r, ag, version) defer sess.Close() @@ -208,36 +210,15 @@ func runPrintMode(ctx context.Context, args Args, version string) error { return err } -// wireNoYoloAutoRefuse installs a BeforeToolExecute hook that -// refuses every tool call when --no-yolo is active but there's no -// interactive UI to prompt (print / json / rpc modes). The reason -// is written in a way the model can learn from so it proposes a -// different action rather than looping on the same tool. -func wireNoYoloAutoRefuse(ag *core.Agent, args Args) { - if !args.NoYolo || ag == nil { - return - } - gate := core.NewConfirmGate(nil) // nil inner = auto-refuse - prev := ag.BeforeToolExecute - ag.BeforeToolExecute = func(call provider.ToolCallBlock) (bool, string, json.RawMessage) { - ok, reason, _ := gate.Check(call.Name, core.BuildPreview(call.Arguments, 120)) - if !ok { - return false, reason, nil - } - if prev != nil { - return prev(call) - } - return true, "", nil - } -} - func runJSONMode(ctx context.Context, args Args, version string) error { + if args.NoYolo { + fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in json mode (no interactive prompt available); tools will run without confirmation") + } r, err := Resolve(args, true) if err != nil { return err } ag := r.NewAgent() - wireNoYoloAutoRefuse(ag, args) sess, _ := openOrCreateSession(args, r, ag, version) defer sess.Close() diff --git a/internal/agent/rpc.go b/internal/agent/rpc.go index e5d7436..c379e76 100644 --- a/internal/agent/rpc.go +++ b/internal/agent/rpc.go @@ -40,6 +40,9 @@ import ( // Auth: if $ZOTCORE_RPC_TOKEN is set, the first command must be // {"type":"hello","token":"..."} or the connection is closed. func runRPCMode(ctx context.Context, args Args, version string) error { + if args.NoYolo { + fmt.Fprintln(os.Stderr, "warning: --no-yolo has no effect in rpc mode (no interactive prompt available); tools will run without confirmation") + } r, err := Resolve(args, true) if err != nil { return err diff --git a/internal/core/confirm.go b/internal/core/confirm.go index c247cbb..cfe6729 100644 --- a/internal/core/confirm.go +++ b/internal/core/confirm.go @@ -53,11 +53,10 @@ type ConfirmGate struct { allowedTool map[string]bool } -// NewConfirmGate returns a gate backed by inner. When inner is nil, -// the gate operates in auto-refuse mode: every tool call is denied -// with a fixed reason. That's what non-interactive modes (-p / -// --json / rpc) use when --no-yolo is on, since they have no way -// to prompt the user. +// NewConfirmGate returns a gate backed by inner. Inner can be nil; +// in that case every not-yet-allowed tool call is refused with a +// fixed reason (the gate is effectively a blocker until AllowAll / +// SetConfirmer is called). func NewConfirmGate(inner Confirmer) *ConfirmGate { return &ConfirmGate{ inner: inner,