package main import ( "context" "fmt" "log/slog" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/exec" ) // terminalToolName 是 exec/terminal 工具的注册名。 const terminalToolName = "terminal" // terminalTool 是 sdk.Tool,通过基于授权的 exec 沙箱(exec.Provider)运行命令。 // 即使沙箱关闭它也会被注册:此时 Execute 返回 exec.ErrExecutionDisabled,因此 // agent 只有在主人显式启用沙箱后才获得执行代码的能力(默认关闭)。 type terminalTool struct { prov exec.Provider log *slog.Logger } // Name 返回工具标识符。 func (t *terminalTool) Name() string { return terminalToolName } // Description 说明工具做什么。 func (t *terminalTool) Description() string { return "Run a terminal command through the sandboxed exec provider and return its stdout, stderr and exit code." } // Parameters 返回输入 JSON Schema。 func (t *terminalTool) Parameters() (map[string]any, error) { return map[string]any{ "type": "object", "properties": map[string]any{ "command": map[string]any{ "type": "string", "description": "The command line to run; split into argv by whitespace (no shell interpretation).", }, "workdir": map[string]any{ "type": "string", "description": "Optional working directory, confined to an allowed sandbox root.", }, "timeout": map[string]any{ "type": "number", "description": "Optional timeout in seconds (default 30).", }, }, "required": []any{"command"}, }, nil } // Execute 通过 exec provider 运行命令并返回结果。沙箱关闭时 provider 返回 // exec.ErrExecutionDisabled,分发器会把它包装成可安全回喂给 LLM 的工具错误。 func (t *terminalTool) Execute(ctx context.Context, args any) (any, error) { m, ok := args.(map[string]any) if !ok { return nil, fmt.Errorf("terminal: args must be an object") } cmd, _ := m["command"].(string) argv := strings.Fields(cmd) if len(argv) == 0 { return nil, fmt.Errorf("terminal: command is required") } if t.log != nil { t.log.Debug("terminal tool invoked", "cmd", strings.Join(argv, " ")) } req := exec.ExecRequest{Command: argv, Timeout: timeoutArg(m["timeout"])} if wd, ok := m["workdir"].(string); ok { req.WorkDir = wd } res, err := t.prov.Execute(ctx, req) if err != nil { return nil, err } return map[string]any{ "stdout": res.Stdout, "stderr": res.Stderr, "exit": res.ExitCode, "truncated": res.Truncated, }, nil } // timeoutArg 把可选的 timeout 参数(秒)转换为 duration。缺失或非正值返回 0, // 由 provider 应用自己的默认值。 func timeoutArg(v any) time.Duration { switch n := v.(type) { case float64: if n > 0 { return time.Duration(n * float64(time.Second)) } case int: if n > 0 { return time.Duration(n) * time.Second } } return 0 } // wireTerminalTool 向分发器注册 terminal 工具,使 agent 能发出沙箱内终端命令。 // 未接入 exec provider 时它只告警并空操作,以保证调用方安全。 func (a *app) wireTerminalTool() { log := logger.WithSource(a.log, "cmd/cyrene") if a.execProv == nil { log.Warn("terminal tool skipped: no exec provider") return } if err := a.dispatcher.Register(&terminalTool{ prov: a.execProv, log: logger.WithSource(a.log, "cmd/cyrene"), }); err != nil { log.Warn("terminal tool register skipped", "error", err) return } log.Info("terminal tool registered", "sandbox_enabled", a.cfg.ExecSandbox.Enabled) }