- 四组并行完成:main/wire*.go、wire_capacity|channels|runtime、channels|persona_*|emotion|observability、 voice|plugins|stream|skills|vision|subagent_tool|extractor|mcp|sessionread_tool|terminal_tool。 - 机械校验(git diff -U0 去 // 行与空行)仅剩 wire_frontend.go:84 一处行尾注释:代码前缀 「\ta.gw = gw 」经 cat -A 与 HEAD 逐字节比对一致(allow 清单已声明)。 - 验收:隔离 worktree(HEAD + 本批 patch)go build/vet 绿、全树 58 包 ok、 linux/amd64 + darwin/arm64 交叉编译绿;gofmt -l 干净。 - 备注:整树两次红均为负载敏感 flake(backthink / orchestration 的 timeout 类用例单独 -count=20 全过)。
119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
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)
|
|
}
|