Files
Agent-System-Backend/internal/tools/exec.go
T
weichenandCursor 2880b58b7b Add run_shell and run_python interpreter executors
Why: deliverable prompts already tell the model to generate Office files
with run_python and register them, but the executors were never
registered, so calls missed the registry and fell back to text-only
write_artifact.

Evidence: go test ./... — 19 packages ok, including echo/script-retention,
empty-script and runtime.exec gate, process-group timeout, python ZIP
magic, and ExecuteTool backfill of register_artifact after run_python.
Scripts stay under the session .agent/ directory; bash/python3 are
resolved to absolute paths; timeout kills the process group.

Impact: models can execute workspace scripts and register binary
deliverables. runtime.exec=false still fail-closes the whole family.
Office extensions now get real MIME types so binary bytes are not
indexed as text.

Refs: task feature/exec-interpreters
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 04:01:58 +00:00

120 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 执行原语工具族(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"
core "agent-system-core"
)
const (
execMaxOutput = 64 << 10 // 单次输出上限 64KB(头尾保留)
execDefaultTimeoutS = 60
execMaxTimeoutS = 300
execMaxArtifactBytes = 10 << 20 // register_artifact 登记上限 10MB
execScriptDir = ".agent" // 脚本落位(会话工作区内,可审计)
execSetting = "runtime.exec"
)
// execGate runtime.exec 总闸(默认开;false = 执行原语整体禁用,fail-closed)。
func execGate(rc ToolExecCtx) error {
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"
}
}