Files
Agent-System-Backend/internal/tools/exec.go
T
weichen 68c1a7272a
ci / vet-and-test (pull_request) Successful in 43s
Keep tracked-change and hyperlink text in DOCX and add an exec hard switch
Why: The DOCX parser only read w:r runs that were direct children of w:p, so
text inside w:ins, w:hyperlink and w:fldSimple was dropped with no warning,
violating the no-silent-degradation rule. Separately, runtime.exec is a
user-writable setting, so multi-user deployments could not reliably turn the
exec primitives off.
Evidence: SELF - red tests TestParseDocxTrackedChangesAndHyperlinks and
TestExecHardDisabledOverridesSettings failed before and pass after; backend
go test ./... (32 packages) passes; a mock-mode binary started with
--disable-exec-tools hides run_shell/run_python/register_artifact from
/api/tools even after the user sets runtime.exec=true.
Impact: DOCX paragraphs are decoded from the token stream at any depth;
deleted/moved-from text, field codes and mc:Fallback are excluded and
revised documents carry an explicit warning. New Options.DisableExecTools
(SurfaceVersion 2.5.0-experimental), CLI --disable-exec-tools, config key
disableExecTools and env AGENT_SYSTEM_DISABLE_EXEC_TOOLS; zero value keeps
current behavior.
Refs: Agent-System-RDM docs/proposals/2026-10-09-V22-review-and-implementation-plan.md (G0-7, B-5)
2026-10-09 10:36:02 +00:00

154 lines
5.6 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"
"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"
}
}