package tools import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" core "agent-system-core" ) type execFake struct { root string owner string conv string execOn bool settings map[string]bool ctx context.Context output []string } func (c *execFake) ProjectID() string { return "p1" } func (c *execFake) ConversationID() string { return c.conv } func (c *execFake) OwnerID() string { return c.owner } func (c *execFake) WorkspaceRoot() string { return c.root } func (c *execFake) Context() context.Context { if c.ctx != nil { return c.ctx } return context.Background() } func (c *execFake) EmitToolOutput(stream, text string) { c.output = append(c.output, stream+":"+text) } func (c *execFake) SettingBool(key string, def bool) bool { if c.settings != nil { if v, ok := c.settings[key]; ok { return v } } if key == execSetting { return c.execOn } return def } func (c *execFake) Retrieve(core.RetrievalQuery) ([]core.RetrievalHit, error) { return nil, nil } func (c *execFake) WriteArtifact(name, kind, mime string, content []byte) (core.Artifact, error) { return core.Artifact{ID: "art_" + name, Name: name, Kind: kind, MIME: mime, Size: int64(len(content))}, nil } func (c *execFake) PeerConversations() ([]core.Conversation, error) { return nil, nil } func (c *execFake) PeerConversationMessages(string, int) ([]core.Message, error) { return nil, nil } func newInterpRegistry(t *testing.T) *Registry { t.Helper() r := NewRegistry() if err := RegisterInterpreterTools(r); err != nil { t.Fatal(err) } return r } func interpCall(t *testing.T, r *Registry, rc *execFake, name, args string) core.ToolCallResult { t.Helper() return r.Execute(rc, core.ToolCallRequest{CallID: "c1", Name: name, Arguments: json.RawMessage(args)}) } func TestRunShellEchoKeepsScript(t *testing.T) { r := newInterpRegistry(t) root := t.TempDir() rc := &execFake{root: root, owner: "o1", conv: "c1", execOn: true} const script = "echo hello-from-shell\n" res := interpCall(t, r, rc, "run_shell", `{"script":"echo hello-from-shell\n"}`) if res.Status != "ok" || !strings.Contains(res.Output, "exit=0") || !strings.Contains(res.Output, "hello-from-shell") { t.Fatalf("echo script: %s %s", res.Status, res.Output) } dir := filepath.Join(root, "o1", "c1", execScriptDir) ents, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } if len(ents) != 1 { t.Fatalf("script should be retained for audit, got %d files", len(ents)) } b, err := os.ReadFile(filepath.Join(dir, ents[0].Name())) if err != nil { t.Fatal(err) } if string(b) != script { t.Fatalf("script bytes mismatch: %q", b) } } func TestRunShellCommandAlias(t *testing.T) { r := newInterpRegistry(t) rc := &execFake{root: t.TempDir(), owner: "o1", conv: "c1", execOn: true} res := interpCall(t, r, rc, "run_shell", `{"command":"echo aliased"}`) if res.Status != "ok" || !strings.Contains(res.Output, "aliased") { t.Fatalf("command alias: %s %s", res.Status, res.Output) } } func TestRunShellEmptyAndGate(t *testing.T) { r := newInterpRegistry(t) rc := &execFake{root: t.TempDir(), owner: "o1", conv: "c1", execOn: true} res := interpCall(t, r, rc, "run_shell", `{"script":" "}`) if res.Status != "error" || !strings.Contains(res.Output, "script required") { t.Fatalf("empty script: %s %s", res.Status, res.Output) } rc.execOn = false res = interpCall(t, r, rc, "run_shell", `{"script":"echo no"}`) if res.Status != "error" || !strings.Contains(res.Output, "runtime.exec") { t.Fatalf("gate off: %s %s", res.Status, res.Output) } } // 启动级硬关闭:用户设置 runtime.exec=true 也无法打开执行原语, // 且执行原语从模型可见清单中消失、内核执行配置为空(fail-closed)。 func TestExecHardDisabledOverridesSettings(t *testing.T) { t.Cleanup(func() { SetExecDisabled(false) }) SetExecDisabled(true) r := newInterpRegistry(t) if err := RegisterExecTools(r); err != nil { t.Fatal(err) } rc := &execFake{root: t.TempDir(), owner: "o1", conv: "c1", execOn: true} for _, name := range []string{"run_shell", "run_python"} { res := interpCall(t, r, rc, name, `{"script":"echo should-not-run"}`) if res.Status != "error" || !strings.Contains(res.Output, "启动参数禁用") { t.Fatalf("%s must be refused under hard disable: %s %s", name, res.Status, res.Output) } } res := interpCall(t, r, rc, "register_artifact", `{"path":"x.docx"}`) if res.Status != "error" || !strings.Contains(res.Output, "启动参数禁用") { t.Fatalf("register_artifact must be refused under hard disable: %s %s", res.Status, res.Output) } if InterpreterExecConfig() != nil { t.Fatal("kernel exec config must be nil under hard disable") } for _, d := range FilterExecDisabled(r.List()) { if IsExecTool(d.Name) { t.Fatalf("exec tool %s must be hidden under hard disable", d.Name) } } SetExecDisabled(false) if res := interpCall(t, r, rc, "run_shell", `{"script":"echo back"}`); res.Status != "ok" { t.Fatalf("exec must work again when not hard-disabled: %s %s", res.Status, res.Output) } if len(FilterExecDisabled(r.List())) != len(r.List()) { t.Fatal("filter must be a no-op when not hard-disabled") } } func TestRunShellStreamsOutput(t *testing.T) { r := newInterpRegistry(t) rc := &execFake{root: t.TempDir(), owner: "o1", conv: "c1", execOn: true} res := interpCall(t, r, rc, "run_shell", `{"script":"echo streamed-line"}`) if res.Status != "ok" || !strings.Contains(res.Output, "streamed-line") { t.Fatalf("result: %s %s", res.Status, res.Output) } joined := strings.Join(rc.output, "") if !strings.Contains(joined, "stdout:") || !strings.Contains(joined, "streamed-line") { t.Fatalf("streamed chunks: %q", rc.output) } } func TestRunShellTimeoutKillsProcessGroup(t *testing.T) { r := newInterpRegistry(t) rc := &execFake{root: t.TempDir(), owner: "o1", conv: "c1", execOn: true} res := interpCall(t, r, rc, "run_shell", `{"timeout_sec":1,"script":"sleep 30 &\nwait\n"}`) if res.Status != "ok" || !strings.Contains(res.Output, "exit=timeout(1s)") { t.Fatalf("timeout: %s %s", res.Status, res.Output) } } func TestRunPythonWritesZipMagic(t *testing.T) { r := newInterpRegistry(t) root := t.TempDir() rc := &execFake{root: root, owner: "o1", conv: "c1", execOn: true} res := interpCall(t, r, rc, "run_python", `{"script":"open('note.docx','wb').write(b'PK\\x03\\x04hello')\n"}`) if res.Status != "ok" || !strings.Contains(res.Output, "exit=0") { t.Fatalf("python: %s %s", res.Status, res.Output) } b, err := os.ReadFile(filepath.Join(root, "o1", "c1", "note.docx")) if err != nil { t.Fatal(err) } if len(b) < 4 || string(b[:4]) != "PK\x03\x04" { t.Fatalf("docx magic: %q", b) } } func TestExecEnvIncludesSkillsRoot(t *testing.T) { prev := skillsRoot t.Cleanup(func() { skillsRoot = prev }) SetSkillsRoot(filepath.Join(t.TempDir(), "skills")) var dir, pythonpath string for _, kv := range execEnv() { if strings.HasPrefix(kv, "AGENT_SKILLS_DIR=") { dir = strings.TrimPrefix(kv, "AGENT_SKILLS_DIR=") } if strings.HasPrefix(kv, "PYTHONPATH=") { pythonpath = strings.TrimPrefix(kv, "PYTHONPATH=") } } if dir != skillsRoot { t.Fatalf("AGENT_SKILLS_DIR = %q", dir) } if pythonpath != filepath.Join(skillsRoot, "house-style") { t.Fatalf("PYTHONPATH = %q", pythonpath) } } func TestInterpreterExecConfigForKernel(t *testing.T) { cfg := InterpreterExecConfig() if cfg == nil { t.Fatal("host has bash/python3; kernel exec config must resolve") } if cfg.Bash == "" && cfg.Python == "" { t.Fatal("at least one interpreter path must be set") } if !filepath.IsAbs(cfg.Bash) && cfg.Bash != "" { t.Fatalf("bash path must be absolute, got %q", cfg.Bash) } for _, kv := range cfg.Env { if strings.HasPrefix(kv, "PATH=") || strings.HasPrefix(kv, "HOME=") || strings.HasPrefix(kv, "LANG=") || strings.HasPrefix(kv, "LC_") || strings.HasPrefix(kv, "TZ=") || strings.HasPrefix(kv, "TERM=") || kv == "PYTHONDONTWRITEBYTECODE=1" || strings.HasPrefix(kv, "AGENT_SKILLS_DIR=") || strings.HasPrefix(kv, "PYTHONPATH=") { continue } t.Fatalf("env whitelist violated: %q", kv) } }