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:
KoheiYamashita 2026-02-17 18:28:48 +09:00
parent 5c6603b34b
commit 461585dbd5
5 changed files with 310 additions and 6 deletions

View file

@ -114,6 +114,15 @@
}
},
"tools": {
"exec": {
"enabled": false
},
"i2c": {
"enabled": false
},
"spi": {
"enabled": false
},
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",

View file

@ -70,8 +70,10 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
registry.Register(tools.NewEditFileTool(workspace, restrict))
registry.Register(tools.NewAppendFileTool(workspace, restrict))
// Shell execution
// Shell execution (disabled by default for security)
if cfg.Tools.Exec.Enabled {
registry.Register(tools.NewExecTool(workspace, restrict))
}
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
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))
// Hardware tools (I2C, SPI) - Linux only, returns error on other platforms
// Hardware tools (I2C, SPI) - disabled by default for security
if cfg.Tools.I2C.Enabled {
registry.Register(tools.NewI2CTool())
}
if cfg.Tools.SPI.Enabled {
registry.Register(tools.NewSPITool())
}
// Message tool - available to both agent and subagent
// Subagent uses it to communicate directly with user

View file

@ -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
func TestAgentLoop_Stop(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")

View file

@ -211,8 +211,23 @@ type WebToolsConfig struct {
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 {
Web WebToolsConfig `json:"web"`
Exec ExecToolsConfig `json:"exec"`
I2C I2CToolsConfig `json:"i2c"`
SPI SPIToolsConfig `json:"spi"`
}
func DefaultConfig() *Config {
@ -311,6 +326,15 @@ func DefaultConfig() *Config {
Port: 18790,
},
Tools: ToolsConfig{
Exec: ExecToolsConfig{
Enabled: false,
},
I2C: I2CToolsConfig{
Enabled: false,
},
SPI: SPIToolsConfig{
Enabled: false,
},
Web: WebToolsConfig{
Brave: BraveConfig{
Enabled: false,

View file

@ -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
func TestDefaultConfig_WebTools(t *testing.T) {
cfg := DefaultConfig()