Skip to content
15 changes: 15 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"path/filepath"

"go-llm-demo/configs"
"go-llm-demo/internal/server/infra/provider"
Comment on lines 7 to 8

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“go-llm-demo” 这个表述或许应该换了,可以在下一个PR中处理。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok,等下一个pr处理掉

Expand All @@ -20,6 +21,10 @@ func main() {
fmt.Printf("设置工作区失败:%v\n", err)
return
}
if err := initializeSecurity(filepath.Join(workspaceRoot, "configs", "security")); err != nil {
fmt.Printf("初始化安全策略失败:%v\n", err)
return
}

if err := configs.LoadAppConfig("config.yaml"); err != nil {
fmt.Printf("加载配置失败:%v\n", err)
Expand Down Expand Up @@ -54,3 +59,13 @@ func main() {
fmt.Printf("Server initialized with services: %+v\n", chatGateway)
fmt.Println("Note: This is a placeholder. Actual server implementation goes here.")
}

func initializeSecurity(configDir string) error {
securityRepo := repository.NewSecurityConfigRepository()
securitySvc := service.NewSecurityService(securityRepo)
if err := securitySvc.Initialize(configDir); err != nil {
return err
}
tools.SetSecurityChecker(securitySvc)
return nil
}
19 changes: 19 additions & 0 deletions cmd/tui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"

"go-llm-demo/configs"
"go-llm-demo/internal/server/infra/repository"
"go-llm-demo/internal/server/infra/tools"
"go-llm-demo/internal/server/service"
"go-llm-demo/internal/tui/bootstrap"

tea "github.com/charmbracelet/bubbletea"
Expand Down Expand Up @@ -56,6 +60,7 @@ func main() {
if err != nil {
os.Exit(1)
}
_ = loadDotEnv(".env")

if err := run(workspaceFlag, os.Stdin, os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down Expand Up @@ -88,6 +93,10 @@ func runWithDeps(workspaceFlag string, deps runDeps) error {
return fmt.Errorf("解析工作区失败: %w", err)
}

if err := initializeSecurity(filepath.Join(workspaceRoot, "configs", "security")); err != nil {
return fmt.Errorf("安全策略初始化失败:%w", err)
}

scanner := bufio.NewScanner(deps.stdin)
ready, err := deps.ensureAPIKeyInteractive(context.Background(), scanner, defaultConfigPath)
if err != nil {
Expand Down Expand Up @@ -121,6 +130,16 @@ func runWithDeps(workspaceFlag string, deps runDeps) error {
return nil
}

func initializeSecurity(configDir string) error {
securityRepo := repository.NewSecurityConfigRepository()
securitySvc := service.NewSecurityService(securityRepo)
if err := securitySvc.Initialize(configDir); err != nil {
return err
}
tools.SetSecurityChecker(securitySvc)
return nil
}

func loadDotEnv(path string) error {
data, err := os.ReadFile(path)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/tui/utf8_other.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//go:build !windows

package main

// setUTF8Mode 在非 Windows 系统上是空操作。
// Linux 和 macOS 默认使用 UTF-8 编码,无需额外设置。
func setUTF8Mode() {}
18 changes: 16 additions & 2 deletions configs/persona.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
- 对不确定的内容明确说明假设
- 默认使用中文回答

你可以调用edit,grep,list,read,write工具,规范和opencode保持一致
安全与工具执行要求(必须遵守):
- 对可能有副作用的操作(尤其是 bash、写文件、网络请求),先进行风险判断,再执行。
- 当命中安全策略为 deny 时,必须明确拒绝并解释原因,不得尝试绕过。
- 当命中安全策略为 ask 时,先向用户确认,再继续执行。
- 执行 bash 前必须说明命令目的、影响范围与关键参数;优先使用只读、可回滚、最小权限命令。
- 严禁执行高危破坏性命令(如无边界删除、系统级破坏、恶意下载执行等)。
- 涉及路径操作时,默认限定在当前工作区内,不对工作区外路径进行读写。

你可以调用edit,grep,list,read,write,bash工具,规范和opencode保持一致

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

模型知道 opencode 是什么吗?
模型知道 opencode 规范是什么吗?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

收到,豆包写的提示词,我这就改


当需要查看指定目录下的文件 / 目录结构时,调用 list 工具。
必填参数:path(目标目录路径,如/home/project);
Expand All @@ -29,10 +37,16 @@
必填参数:filePath(目标文件路径)old_str(待替换的旧文本)、new_str(替换后的新文本);
可选参数:backup(是否创建文件备份,布尔值,默认 true)

当需要在工作区目录内执行任意终端命令、脚本、系统操作时,调用 bash 工具。
必填参数:command(待执行的 bash 命令字符串,如 grep 'error' ./log)
可选参数:workdir(命令执行目录,字符串,默认工作区根目录),timeout(命令超时时间,整数,单位毫秒,默认 120000),description(命令用途说明,字符串,默认空)
Comment on lines +40 to +42

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这些不应该放在系统提示词里,你们的工具定义中已经有 tool params 的 schema 了吧

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是的,历史遗留,这个pr就处理

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

看了一下,工具定义我还没加到上下文,这个可能要等到下个pr修,这个pr管不来这么多

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可以,这些问题。下一个PR要做的事情,都可以先记录一个issue


你必须严格按照以下JSON格式返回工具调用指令,不要返回其他内容:
{
"tool": "工具名称(如list/read/bash)",
"params": {"参数名": "参数值"},
"thought": "你调用该工具的原因"
}
}

调用工具时,你只能严格遵守json格式,不能输出多余字符,一次只能调用一个工具
输出json完成后,主动终止对话,等待工具响应

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“主动终止对话,等待工具响应”

这个不是模型的行为吧(这个 loop 本质上还是我们自己控制的)

所以这里你告诉模型这些信息,它也理解不了,甚至可能会是一种噪音。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

明白了,主要是开始调试没有Todo list的时候有一些思考模型不会终止流式对话,会出现2次输出,为了方便调试加的,这就删

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

看了一下,工具定义我还没加到上下文,这个可能要等到下个pr修,这个pr管不来这么多

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

看了一下,工具定义我还没加到上下文,这个可能要等到下个pr修,这个pr管不来这么多

这个评论不是在这里的吧?

8 changes: 8 additions & 0 deletions configs/persona.txt.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@
- 回答简洁清晰,必要时给出步骤
- 对不确定的内容明确说明假设
- 默认使用中文回答

安全与工具执行要求(必须遵守):
- 对可能有副作用的操作(尤其是 bash、写文件、网络请求),先进行风险判断,再执行。
- 当命中安全策略为 deny 时,必须明确拒绝并解释原因,不得尝试绕过。
- 当命中安全策略为 ask 时,先向用户确认,再继续执行。
- 执行 bash 前必须说明命令目的、影响范围与关键参数;优先使用只读、可回滚、最小权限命令。
- 严禁执行高危破坏性命令(如无边界删除、系统级破坏、恶意下载执行等)。
- 涉及路径操作时,默认限定在当前工作区内,不对工作区外路径进行读写。
52 changes: 51 additions & 1 deletion internal/server/infra/tools/bash.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"os/exec"
"runtime"
"time"
)

Expand All @@ -30,6 +31,9 @@ func (b *BashTool) Run(params map[string]interface{}) *ToolResult {
errRes.ToolName = b.Definition().Name
return errRes
}
if denied := guardToolExecution("Bash", command, b.Definition().Name); denied != nil {
return denied
}
timeoutMs, errRes := optionalInt(params, "timeout", 120000)
if errRes != nil {
errRes.ToolName = b.Definition().Name
Expand All @@ -52,7 +56,26 @@ func (b *BashTool) Run(params map[string]interface{}) *ToolResult {

ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, "bash", "-lc", command)

var shell string
var shellArgs []string
switch runtime.GOOS {
case "linux", "darwin":
// Linux/macOS: 使用 bash
shell = "bash"
shellArgs = []string{"-lc", command}
case "windows":
// Windows: 使用 PowerShell
shell = "powershell"
shellArgs = []string{"-Command", command}
default:
shell = "bash"
shellArgs = []string{"-lc", command}
}

// 使用动态选择的 shell 和参数创建命令
shell, shellArgs = preferredShellCommand(runtime.GOOS, command, exec.LookPath, shell, shellArgs)
cmd := exec.CommandContext(ctx, shell, shellArgs...)
cmd.Dir = workdir
var stdoutBuf, stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
Expand All @@ -79,3 +102,30 @@ func (b *BashTool) Run(params map[string]interface{}) *ToolResult {
}
return result
}

type shellLookup func(string) (string, error)

func preferredShellCommand(goos, command string, lookPath shellLookup, shell string, shellArgs []string) (string, []string) {
switch goos {
case "windows":
for _, candidate := range []struct {
name string
args []string
}{
{name: "powershell", args: []string{"-Command", command}},
{name: "pwsh", args: []string{"-Command", command}},
{name: "cmd.exe", args: []string{"/C", command}},
{name: "cmd", args: []string{"/C", command}},
} {
if _, err := lookPath(candidate.name); err == nil {
return candidate.name, candidate.args
}
}
return "cmd", []string{"/C", command}
default:
if _, err := lookPath("bash"); err == nil {
return "bash", []string{"-lc", command}
}
return "sh", []string{"-c", command}
}
}
95 changes: 95 additions & 0 deletions internal/server/infra/tools/bash_security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package tools

import (
"errors"
"strings"
"testing"

"go-llm-demo/internal/server/domain"
)

type mockSecurityChecker struct {
action domain.Action
}

func (m mockSecurityChecker) Check(_ string, _ string) domain.Action {
return m.action
}

func TestBashTool_Run_DeniedBySecurity(t *testing.T) {
SetSecurityChecker(mockSecurityChecker{action: domain.ActionDeny})
defer SetSecurityChecker(nil)

result := (&BashTool{}).Run(map[string]interface{}{
"command": "echo hello",
})

if result == nil {
t.Fatal("result should not be nil")
}
if result.Success {
t.Fatal("expected bash execution to be denied")
}
if !strings.Contains(result.Error, "安全策略拒绝执行") {
t.Fatalf("unexpected error: %s", result.Error)
}
if result.Metadata["securityAction"] != string(domain.ActionDeny) {
t.Fatalf("unexpected security action: %#v", result.Metadata["securityAction"])
}
}

func TestBashTool_Run_AskBySecurity(t *testing.T) {
SetSecurityChecker(mockSecurityChecker{action: domain.ActionAsk})
defer SetSecurityChecker(nil)

result := (&BashTool{}).Run(map[string]interface{}{
"command": "go build ./...",
})

if result == nil {
t.Fatal("result should not be nil")
}
if result.Success {
t.Fatal("expected bash execution to require confirmation")
}
if !strings.Contains(result.Error, "需要用户确认") {
t.Fatalf("unexpected error: %s", result.Error)
}
if result.Metadata["securityAction"] != string(domain.ActionAsk) {
t.Fatalf("unexpected security action: %#v", result.Metadata["securityAction"])
}
}

func TestPreferredShellCommandFallsBackToCmdOnWindows(t *testing.T) {
lookup := func(name string) (string, error) {
if name == "cmd.exe" {
return name, nil
}
return "", errors.New("not found")
}

shell, args := preferredShellCommand("windows", "echo hello", lookup, "powershell", []string{"-Command", "echo hello"})
if shell != "cmd.exe" {
t.Fatalf("expected cmd.exe fallback, got %q", shell)
}
if len(args) != 2 || args[0] != "/C" || args[1] != "echo hello" {
t.Fatalf("unexpected cmd.exe args: %#v", args)
}
}

func TestPreferredShellCommandFallsBackToShWithoutBash(t *testing.T) {
lookup := func(name string) (string, error) {
if name == "sh" {
return name, nil
}
return "", errors.New("not found")
}

shell, args := preferredShellCommand("linux", "echo hello", lookup, "bash", []string{"-lc", "echo hello"})
if shell != "sh" {
t.Fatalf("expected sh fallback, got %q", shell)
}
if len(args) != 2 || args[0] != "-c" || args[1] != "echo hello" {
t.Fatalf("unexpected sh args: %#v", args)
}
}
Loading