// 执行原语工具族(agent-native 交付,2026-10-01 用户定调):文档/表格/演示稿 // 等产物生成是 Agent 的能力——系统只提供原语与登记桥,生成方法沉淀在提示词库 // (base.deliverables 配方),不提供 write_docx 这类领域工具。 // // 信任模型:本地单用户工作台(单二进制自托管)。run_shell/run_python 的脚本 // 一律先落盘为会话工作区内的文件再交给白名单解释器(bash/python3)执行, // 工作目录锁定会话沙箱,超时与输出上限硬性截断;画像三清单与 runtime.exec // 开关可禁用(fail-closed)。脚本落盘保留审计痕迹。 package tools import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "sync/atomic" core "agent-system-core" ) const ( execMaxOutput = 64 << 10 // 单次输出上限 64KB(头尾保留) execDefaultTimeoutS = 600 // 未指定 timeout_sec 时的默认超时;模型可自行设更长,没有硬上限 execMaxArtifactBytes = 10 << 20 // register_artifact 登记上限 10MB execScriptDir = ".agent" // 脚本落位(会话工作区内,可审计) execSetting = "runtime.exec" ) // execHardOff 启动级硬关闭(组合根依启动参数设置)。runtime.exec 是用户可写的 // 业务设置,多用户部署时任何人都能把它改回 true;硬关闭不读设置,设置无法覆盖。 var execHardOff atomic.Bool // SetExecDisabled 设置启动级硬关闭;仅组合根在装配期调用。 func SetExecDisabled(off bool) { execHardOff.Store(off) } // ExecDisabled 报告是否处于启动级硬关闭。 func ExecDisabled() bool { return execHardOff.Load() } // execToolNames 受硬关闭约束的执行原语。 var execToolNames = map[string]bool{"run_shell": true, "run_python": true, "register_artifact": true} // IsExecTool 报告工具是否属于执行原语族。 func IsExecTool(name string) bool { return execToolNames[name] } // FilterExecDisabled 硬关闭时从工具清单移除执行原语(可见性一侧;执行一侧由 execGate 拒绝)。 func FilterExecDisabled(list []core.ToolDesc) []core.ToolDesc { if !ExecDisabled() { return list } out := make([]core.ToolDesc, 0, len(list)) for _, t := range list { if !IsExecTool(t.Name) { out = append(out, t) } } return out } // execGate 执行原语总闸:启动级硬关闭优先;其后 runtime.exec(默认开;false = // 执行原语整体禁用,fail-closed)。 func execGate(rc ToolExecCtx) error { if ExecDisabled() { return fmt.Errorf("exec tools: 已被启动参数禁用(disableExecTools)") } if !rc.SettingBool(execSetting, true) { return fmt.Errorf("exec tools: 已被设置禁用(%s=false)", execSetting) } return nil } // execSandbox 返回(并确保存在)会话沙箱目录。 func execSandbox(rc ToolExecCtx) (string, error) { sandbox, err := fsSandbox(rc) if err != nil { return "", err } if err := os.MkdirAll(sandbox, 0o755); err != nil { return "", fmt.Errorf("exec tools: create sandbox: %w", err) } return sandbox, nil } // RegisterExecTools 注册执行原语族的登记桥(register_artifact)。 // run_shell / run_python 在 exec_interpreter.go,由 RegisterInterpreterTools 同批注册 // (契约见 base.deliverables 提示词)。 func RegisterExecTools(r *Registry) error { return r.Register(core.ToolDesc{ Name: "register_artifact", Source: core.SourceBuiltin, Description: "把会话工作区内已生成的文件登记为交付物工件(docx/xlsx/pptx/pdf/图片/数据等):登记后文件出现在对话交付卡片与工件面板,可预览、下载。二进制文件不能用 fs_write_file(只收文本)——先由 run_python/run_shell 生成到工作区,再用本工具登记。", Parameters: json.RawMessage(`{"type":"object","properties":{` + `"path":{"type":"string","description":"工作区内相对路径(如 报告.docx)"}}` + `,"required":["path"]}`), }, execRegisterArtifact) } func execRegisterArtifact(rc ToolExecCtx, args json.RawMessage) (string, error) { if err := execGate(rc); err != nil { return "", err } var a struct { Path string `json:"path"` } if len(args) > 0 { if err := json.Unmarshal(args, &a); err != nil { return "", fmt.Errorf("register_artifact: bad args: %w", err) } } if strings.TrimSpace(a.Path) == "" { return "", fmt.Errorf("register_artifact: path required") } sandbox, err := execSandbox(rc) if err != nil { return "", err } abs, err := fsResolve(sandbox, a.Path) if err != nil { return "", fmt.Errorf("register_artifact: %w", err) } st, err := os.Stat(abs) if err != nil { return "", fmt.Errorf("register_artifact: %s: %v", a.Path, err) } if st.IsDir() { return "", fmt.Errorf("register_artifact: %s 是目录(逐个文件登记)", a.Path) } if st.Size() > execMaxArtifactBytes { return "", fmt.Errorf("register_artifact: 文件超过 10MB 上限(%d 字节)", st.Size()) } b, err := os.ReadFile(abs) if err != nil { return "", err } name := filepath.Base(abs) mime := mimeOfExt(name) art, err := rc.WriteArtifact(name, artifactKindOf(name, mime), mime, b) if err != nil { return "", err } return fmt.Sprintf("已登记交付物 %s(%s,%d 字节)——对话内会出现可预览的文件卡片", art.Name, art.ID, art.Size), nil } // artifactKindOf 按扩展名/mime 归类工件 kind(工件面板过滤与卡片图标用)。 func artifactKindOf(name, mime string) string { switch { case strings.HasPrefix(mime, "image/"): return "image" case strings.Contains(mime, "json") || strings.Contains(mime, "csv"): return "data" default: return "document" } }