feat(security): disable exec, i2c, spi tools by default
Security hardening: exec, I2C, SPI tools are now disabled by default
and require explicit opt-in via config or environment variables.
Users who need these tools must set enabled: true in config.json
or use PICOCLAW_TOOLS_{EXEC,I2C,SPI}_ENABLED=true.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5c6603b34b
commit
461585dbd5
5 changed files with 310 additions and 6 deletions
|
|
@ -114,6 +114,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"exec": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"i2c": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"spi": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"search": {
|
"search": {
|
||||||
"api_key": "YOUR_BRAVE_API_KEY",
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,10 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
|
||||||
registry.Register(tools.NewEditFileTool(workspace, restrict))
|
registry.Register(tools.NewEditFileTool(workspace, restrict))
|
||||||
registry.Register(tools.NewAppendFileTool(workspace, restrict))
|
registry.Register(tools.NewAppendFileTool(workspace, restrict))
|
||||||
|
|
||||||
// Shell execution
|
// Shell execution (disabled by default for security)
|
||||||
registry.Register(tools.NewExecTool(workspace, restrict))
|
if cfg.Tools.Exec.Enabled {
|
||||||
|
registry.Register(tools.NewExecTool(workspace, restrict))
|
||||||
|
}
|
||||||
|
|
||||||
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
||||||
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
|
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
|
||||||
|
|
@ -84,9 +86,13 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
|
||||||
}
|
}
|
||||||
registry.Register(tools.NewWebFetchTool(50000))
|
registry.Register(tools.NewWebFetchTool(50000))
|
||||||
|
|
||||||
// Hardware tools (I2C, SPI) - Linux only, returns error on other platforms
|
// Hardware tools (I2C, SPI) - disabled by default for security
|
||||||
registry.Register(tools.NewI2CTool())
|
if cfg.Tools.I2C.Enabled {
|
||||||
registry.Register(tools.NewSPITool())
|
registry.Register(tools.NewI2CTool())
|
||||||
|
}
|
||||||
|
if cfg.Tools.SPI.Enabled {
|
||||||
|
registry.Register(tools.NewSPITool())
|
||||||
|
}
|
||||||
|
|
||||||
// Message tool - available to both agent and subagent
|
// Message tool - available to both agent and subagent
|
||||||
// Subagent uses it to communicate directly with user
|
// Subagent uses it to communicate directly with user
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,247 @@ func TestAgentLoop_GetStartupInfo(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCreateToolRegistry_ExecDisabled verifies exec tool is NOT registered when disabled
|
||||||
|
func TestCreateToolRegistry_ExecDisabled(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tools: config.ToolsConfig{
|
||||||
|
Exec: config.ExecToolsConfig{
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &mockProvider{}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
info := al.GetStartupInfo()
|
||||||
|
toolsInfo := info["tools"].(map[string]interface{})
|
||||||
|
toolsList := toolsInfo["names"].([]string)
|
||||||
|
|
||||||
|
for _, name := range toolsList {
|
||||||
|
if name == "exec" {
|
||||||
|
t.Error("exec tool should NOT be registered when Exec.Enabled is false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateToolRegistry_ExecEnabled verifies exec tool IS registered when enabled
|
||||||
|
func TestCreateToolRegistry_ExecEnabled(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tools: config.ToolsConfig{
|
||||||
|
Exec: config.ExecToolsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &mockProvider{}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
info := al.GetStartupInfo()
|
||||||
|
toolsInfo := info["tools"].(map[string]interface{})
|
||||||
|
toolsList := toolsInfo["names"].([]string)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, name := range toolsList {
|
||||||
|
if name == "exec" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("exec tool should be registered when Exec.Enabled is true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateToolRegistry_I2CDisabled verifies I2C tool is NOT registered when disabled
|
||||||
|
func TestCreateToolRegistry_I2CDisabled(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tools: config.ToolsConfig{
|
||||||
|
I2C: config.I2CToolsConfig{Enabled: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &mockProvider{}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
info := al.GetStartupInfo()
|
||||||
|
toolsInfo := info["tools"].(map[string]interface{})
|
||||||
|
toolsList := toolsInfo["names"].([]string)
|
||||||
|
|
||||||
|
for _, name := range toolsList {
|
||||||
|
if name == "i2c" {
|
||||||
|
t.Error("i2c tool should NOT be registered when I2C.Enabled is false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateToolRegistry_I2CEnabled verifies I2C tool IS registered when enabled
|
||||||
|
func TestCreateToolRegistry_I2CEnabled(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tools: config.ToolsConfig{
|
||||||
|
I2C: config.I2CToolsConfig{Enabled: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &mockProvider{}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
info := al.GetStartupInfo()
|
||||||
|
toolsInfo := info["tools"].(map[string]interface{})
|
||||||
|
toolsList := toolsInfo["names"].([]string)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, name := range toolsList {
|
||||||
|
if name == "i2c" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("i2c tool should be registered when I2C.Enabled is true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateToolRegistry_SPIDisabled verifies SPI tool is NOT registered when disabled
|
||||||
|
func TestCreateToolRegistry_SPIDisabled(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tools: config.ToolsConfig{
|
||||||
|
SPI: config.SPIToolsConfig{Enabled: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &mockProvider{}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
info := al.GetStartupInfo()
|
||||||
|
toolsInfo := info["tools"].(map[string]interface{})
|
||||||
|
toolsList := toolsInfo["names"].([]string)
|
||||||
|
|
||||||
|
for _, name := range toolsList {
|
||||||
|
if name == "spi" {
|
||||||
|
t.Error("spi tool should NOT be registered when SPI.Enabled is false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCreateToolRegistry_SPIEnabled verifies SPI tool IS registered when enabled
|
||||||
|
func TestCreateToolRegistry_SPIEnabled(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tools: config.ToolsConfig{
|
||||||
|
SPI: config.SPIToolsConfig{Enabled: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
provider := &mockProvider{}
|
||||||
|
al := NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
info := al.GetStartupInfo()
|
||||||
|
toolsInfo := info["tools"].(map[string]interface{})
|
||||||
|
toolsList := toolsInfo["names"].([]string)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, name := range toolsList {
|
||||||
|
if name == "spi" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("spi tool should be registered when SPI.Enabled is true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestAgentLoop_Stop verifies Stop() sets running to false
|
// TestAgentLoop_Stop verifies Stop() sets running to false
|
||||||
func TestAgentLoop_Stop(t *testing.T) {
|
func TestAgentLoop_Stop(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
|
|
||||||
|
|
@ -211,8 +211,23 @@ type WebToolsConfig struct {
|
||||||
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
|
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExecToolsConfig struct {
|
||||||
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_EXEC_ENABLED"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type I2CToolsConfig struct {
|
||||||
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_I2C_ENABLED"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SPIToolsConfig struct {
|
||||||
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_SPI_ENABLED"`
|
||||||
|
}
|
||||||
|
|
||||||
type ToolsConfig struct {
|
type ToolsConfig struct {
|
||||||
Web WebToolsConfig `json:"web"`
|
Web WebToolsConfig `json:"web"`
|
||||||
|
Exec ExecToolsConfig `json:"exec"`
|
||||||
|
I2C I2CToolsConfig `json:"i2c"`
|
||||||
|
SPI SPIToolsConfig `json:"spi"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
|
|
@ -311,6 +326,15 @@ func DefaultConfig() *Config {
|
||||||
Port: 18790,
|
Port: 18790,
|
||||||
},
|
},
|
||||||
Tools: ToolsConfig{
|
Tools: ToolsConfig{
|
||||||
|
Exec: ExecToolsConfig{
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
I2C: I2CToolsConfig{
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
SPI: SPIToolsConfig{
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
Web: WebToolsConfig{
|
Web: WebToolsConfig{
|
||||||
Brave: BraveConfig{
|
Brave: BraveConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,30 @@ func TestDefaultConfig_Channels(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDefaultConfig_ExecToolDisabled verifies exec tool is disabled by default
|
||||||
|
func TestDefaultConfig_ExecToolDisabled(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if cfg.Tools.Exec.Enabled {
|
||||||
|
t.Error("Exec tool should be disabled by default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDefaultConfig_I2CToolDisabled verifies I2C tool is disabled by default
|
||||||
|
func TestDefaultConfig_I2CToolDisabled(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if cfg.Tools.I2C.Enabled {
|
||||||
|
t.Error("I2C tool should be disabled by default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDefaultConfig_SPIToolDisabled verifies SPI tool is disabled by default
|
||||||
|
func TestDefaultConfig_SPIToolDisabled(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if cfg.Tools.SPI.Enabled {
|
||||||
|
t.Error("SPI tool should be disabled by default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestDefaultConfig_WebTools verifies web tools config
|
// TestDefaultConfig_WebTools verifies web tools config
|
||||||
func TestDefaultConfig_WebTools(t *testing.T) {
|
func TestDefaultConfig_WebTools(t *testing.T) {
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue