feat(exec): 本地执行 Provider + 放权终端沙箱(白名单/工作目录/超时/输出截断/默认关)+ 文件沙箱;config 增 ExecSandbox(默认关)

This commit is contained in:
Aska committed 2026-09-06 06:59:23 +08:00
1 parent 88dc12466d
commit 430fc4666b
11 files changed
+1199 -20

No files matched your search

+21 -20
View File
@@ -15,21 +15,22 @@ import (
// Config is the root configuration tree.
type Config struct {
Env string `env:"ENV" default:"development"`
LogLevel string `env:"LOG_LEVEL" default:"debug"`
Timezone string `env:"TZ" default:"Asia/Shanghai"`
DataDir string `env:"DATA_DIR" default:"./data"`
AllowedOrigins []string `env:"ALLOWED_ORIGINS" default:"http://localhost:5173,http://localhost:3000"`
Env string `env:"ENV" default:"development"`
LogLevel string `env:"LOG_LEVEL" default:"debug"`
Timezone string `env:"TZ" default:"Asia/Shanghai"`
DataDir string `env:"DATA_DIR" default:"./data"`
AllowedOrigins []string `env:"ALLOWED_ORIGINS" default:"http://localhost:5173,http://localhost:3000"`
Server ServerConfig
Auth AuthConfig
DB DBConfig
LLM LLMConfig
Embedding EmbeddingConfig
IoT IoTConfig
Voice VoiceConfig
Background BackgroundConfig
WSHub WSHubConfig
Server ServerConfig
Auth AuthConfig
DB DBConfig
LLM LLMConfig
Embedding EmbeddingConfig
IoT IoTConfig
Voice VoiceConfig
Background BackgroundConfig
WSHub WSHubConfig
ExecSandbox ExecSandbox
}
// ServerConfig holds the single gateway listener settings.
@@ -76,16 +77,16 @@ type IoTConfig struct {
// VoiceConfig holds TTS/STT provider settings.
type VoiceConfig struct {
TTSProvider string `env:"TTS_PROVIDER" default:"edge-tts"`
STTProvider string `env:"STT_PROVIDER" default:"dashscope"`
DashscopeAPIKey string `env:"DASHSCOPE_API_KEY"`
TTSProvider string `env:"TTS_PROVIDER" default:"edge-tts"`
STTProvider string `env:"STT_PROVIDER" default:"dashscope"`
DashscopeAPIKey string `env:"DASHSCOPE_API_KEY"`
}
// BackgroundConfig holds the autonomous thinking settings.
type BackgroundConfig struct {
Enabled bool `env:"ENABLE_BACKGROUND_THINKING" default:"true"`
IntervalSec int `env:"THINK_INTERVAL_SEC" default:"300"`
SilenceTimeout int `env:"THINK_SILENCE_TIMEOUT_SEC" default:"120"`
Enabled bool `env:"ENABLE_BACKGROUND_THINKING" default:"true"`
IntervalSec int `env:"THINK_INTERVAL_SEC" default:"300"`
SilenceTimeout int `env:"THINK_SILENCE_TIMEOUT_SEC" default:"120"`
}
// WSHubConfig holds the WebSocket hub limits.
+24
View File
@@ -0,0 +1,24 @@
package config
// ExecSandbox holds the grant-based local execution sandbox settings
// (DevDocs/design/04-运行时.md §8 and design/11 §5). The sandbox is closed by
// default: the agent may run commands only when Enabled is true, and even then
// only within the command allowlist and the root boundaries configured here.
type ExecSandbox struct {
// Enabled opens the sandbox. It is false by default — the agent cannot
// execute anything unless the owner explicitly enables it. This is the
// "default off, explicit grant" red line.
Enabled bool `env:"EXEC_SANDBOX_ENABLED" default:"false"`
// CommandList is the allowlist of command names (argv[0]) the agent may run.
// An empty list denies every command (fail closed).
CommandList []string `env:"EXEC_SANDBOX_COMMANDS"`
// AllowedRoots are the directories commands and files are confined to.
// Working directories and file paths must resolve inside one of them.
AllowedRoots []string `env:"EXEC_SANDBOX_ROOTS"`
// AllowedEnv lists the environment variable names the agent may set on a
// subprocess. Variable names outside this list are rejected.
AllowedEnv []string `env:"EXEC_SANDBOX_ENV"`
}
+51
View File
@@ -0,0 +1,51 @@
package config
import (
"reflect"
"strings"
"testing"
)
func TestExecSandbox_DefaultDisabled(t *testing.T) {
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
t.Setenv("EXEC_SANDBOX_ENABLED", "false")
t.Setenv("EXEC_SANDBOX_COMMANDS", "")
t.Setenv("EXEC_SANDBOX_ROOTS", "")
t.Setenv("EXEC_SANDBOX_ENV", "")
cfg, err := Load("")
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if cfg.ExecSandbox.Enabled {
t.Errorf("ExecSandbox.Enabled = true, want default false (sandbox closed)")
}
if len(cfg.ExecSandbox.CommandList) != 0 {
t.Errorf("CommandList = %v, want empty", cfg.ExecSandbox.CommandList)
}
}
func TestExecSandbox_EnvOverrides(t *testing.T) {
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
t.Setenv("EXEC_SANDBOX_ENABLED", "true")
t.Setenv("EXEC_SANDBOX_COMMANDS", "echo,pwd")
t.Setenv("EXEC_SANDBOX_ROOTS", "/tmp/sandbox,/home/me/workspace")
t.Setenv("EXEC_SANDBOX_ENV", "PATH,HOME")
cfg, err := Load("")
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if !cfg.ExecSandbox.Enabled {
t.Errorf("ExecSandbox.Enabled = false, want true")
}
if want := []string{"echo", "pwd"}; !reflect.DeepEqual(cfg.ExecSandbox.CommandList, want) {
t.Errorf("CommandList = %v, want %v", cfg.ExecSandbox.CommandList, want)
}
if want := []string{"/tmp/sandbox", "/home/me/workspace"}; !reflect.DeepEqual(cfg.ExecSandbox.AllowedRoots, want) {
t.Errorf("AllowedRoots = %v, want %v", cfg.ExecSandbox.AllowedRoots, want)
}
if want := []string{"PATH", "HOME"}; !reflect.DeepEqual(cfg.ExecSandbox.AllowedEnv, want) {
t.Errorf("AllowedEnv = %v, want %v", cfg.ExecSandbox.AllowedEnv, want)
}
}