diff --git a/.infer/config.yaml b/.infer/config.yaml index e57651b8..7e91c474 100644 --- a/.infer/config.yaml +++ b/.infer/config.yaml @@ -12,9 +12,6 @@ tools: - ls - pwd - echo - - cat - - head - - tail - grep - find - wc @@ -27,7 +24,10 @@ tools: - ^kubectl get pods$ safety: require_approval: true + exclude_paths: + - .infer/ + - .infer/* compact: output_dir: .infer chat: - default_model: deepseek/deepseek-chat + default_model: "" diff --git a/CLAUDE.md b/CLAUDE.md index 6d5577c8..f5358ae4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -152,6 +152,9 @@ tools: - "^kubectl get pods$" safety: require_approval: true # Prompt user before executing any command + exclude_paths: # Paths excluded from tool access for security + - ".infer/" # Protect infer's own configuration directory + - ".infer/*" # Protect all files in infer's configuration directory compact: output_dir: ".infer" # Directory for compact command exports (default: project root/.infer) chat: @@ -177,6 +180,10 @@ chat: - `enable`: Enable safety approval prompts - `disable`: Disable safety approval prompts - `status`: Show current safety approval status + - `exclude-path`: Manage excluded paths for security + - `list`: List all excluded paths + - `add `: Add a path to the exclusion list + - `remove `: Remove a path from the exclusion list - `version`: Version information ## Dependencies @@ -245,6 +252,11 @@ infer config tools exec "git status" infer config tools safety enable infer config tools safety disable infer config tools safety status + +# Manage excluded paths for security +infer config tools exclude-path list +infer config tools exclude-path add ".github/" +infer config tools exclude-path remove "test.txt" ``` ## Code Style Guidelines diff --git a/cmd/config.go b/cmd/config.go index aa814b92..075d3a18 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -130,6 +130,35 @@ var configToolsSafetyStatusCmd = &cobra.Command{ RunE: safetyStatus, } +var configToolsExcludePathCmd = &cobra.Command{ + Use: "exclude-path", + Short: "Manage excluded paths", + Long: `Manage paths that are excluded from tool access for security purposes.`, +} + +var configToolsExcludePathListCmd = &cobra.Command{ + Use: "list", + Short: "List excluded paths", + Long: `Display all paths that are excluded from tool access.`, + RunE: listExcludedPaths, +} + +var configToolsExcludePathAddCmd = &cobra.Command{ + Use: "add ", + Short: "Add a path to the exclusion list", + Long: `Add a path to the exclusion list to prevent tools from accessing it.`, + Args: cobra.ExactArgs(1), + RunE: addExcludedPath, +} + +var configToolsExcludePathRemoveCmd = &cobra.Command{ + Use: "remove ", + Short: "Remove a path from the exclusion list", + Long: `Remove a path from the exclusion list to allow tools to access it again.`, + Args: cobra.ExactArgs(1), + RunE: removeExcludedPath, +} + func setDefaultModel(modelName string) error { cfg, err := config.LoadConfig("") if err != nil { @@ -158,11 +187,16 @@ func init() { configToolsCmd.AddCommand(configToolsValidateCmd) configToolsCmd.AddCommand(configToolsExecCmd) configToolsCmd.AddCommand(configToolsSafetyCmd) + configToolsCmd.AddCommand(configToolsExcludePathCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyEnableCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyDisableCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyStatusCmd) + configToolsExcludePathCmd.AddCommand(configToolsExcludePathListCmd) + configToolsExcludePathCmd.AddCommand(configToolsExcludePathAddCmd) + configToolsExcludePathCmd.AddCommand(configToolsExcludePathRemoveCmd) + configInitCmd.Flags().Bool("overwrite", false, "Overwrite existing configuration file") configToolsListCmd.Flags().StringP("format", "f", "text", "Output format (text, json)") configToolsExecCmd.Flags().StringP("format", "f", "text", "Output format (text, json)") @@ -208,9 +242,10 @@ func listTools(cmd *cobra.Command, args []string) error { if format == "json" { data := map[string]interface{}{ - "enabled": cfg.Tools.Enabled, - "commands": cfg.Tools.Whitelist.Commands, - "patterns": cfg.Tools.Whitelist.Patterns, + "enabled": cfg.Tools.Enabled, + "commands": cfg.Tools.Whitelist.Commands, + "patterns": cfg.Tools.Whitelist.Patterns, + "exclude_paths": cfg.Tools.ExcludePaths, "safety": map[string]bool{ "require_approval": cfg.Tools.Safety.RequireApproval, }, @@ -240,6 +275,15 @@ func listTools(cmd *cobra.Command, args []string) error { fmt.Printf(" • %s\n", pattern) } + fmt.Printf("\nExcluded Paths (%d):\n", len(cfg.Tools.ExcludePaths)) + if len(cfg.Tools.ExcludePaths) == 0 { + fmt.Printf(" • None\n") + } else { + for _, path := range cfg.Tools.ExcludePaths { + fmt.Printf(" • %s\n", path) + } + } + fmt.Printf("\nSafety Settings:\n") if cfg.Tools.Safety.RequireApproval { fmt.Printf(" • Approval required: %s\n", ui.FormatSuccess("Enabled")) @@ -381,3 +425,69 @@ func getConfigPath() string { } return configPath } + +func listExcludedPaths(cmd *cobra.Command, args []string) error { + cfg, err := config.LoadConfig("") + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + if len(cfg.Tools.ExcludePaths) == 0 { + fmt.Println("No paths are currently excluded.") + return nil + } + + fmt.Printf("Excluded Paths (%d):\n", len(cfg.Tools.ExcludePaths)) + for _, path := range cfg.Tools.ExcludePaths { + fmt.Printf(" • %s\n", path) + } + + return nil +} + +func addExcludedPath(cmd *cobra.Command, args []string) error { + pathToAdd := args[0] + + _, err := loadAndUpdateConfig(func(c *config.Config) { + for _, existingPath := range c.Tools.ExcludePaths { + if existingPath == pathToAdd { + return + } + } + c.Tools.ExcludePaths = append(c.Tools.ExcludePaths, pathToAdd) + }) + if err != nil { + return err + } + + fmt.Printf("%s\n", ui.FormatSuccess(fmt.Sprintf("Added '%s' to excluded paths", pathToAdd))) + fmt.Printf("Tools will no longer be able to access this path\n") + return nil +} + +func removeExcludedPath(cmd *cobra.Command, args []string) error { + pathToRemove := args[0] + var found bool + + _, err := loadAndUpdateConfig(func(c *config.Config) { + for i, existingPath := range c.Tools.ExcludePaths { + if existingPath == pathToRemove { + c.Tools.ExcludePaths = append(c.Tools.ExcludePaths[:i], c.Tools.ExcludePaths[i+1:]...) + found = true + return + } + } + }) + if err != nil { + return err + } + + if !found { + fmt.Printf("%s\n", ui.FormatWarning(fmt.Sprintf("Path '%s' was not in the excluded paths list", pathToRemove))) + return nil + } + + fmt.Printf("%s\n", ui.FormatSuccess(fmt.Sprintf("Removed '%s' from excluded paths", pathToRemove))) + fmt.Printf("Tools can now access this path again\n") + return nil +} diff --git a/config/config.go b/config/config.go index 7bf5db19..accf28f3 100644 --- a/config/config.go +++ b/config/config.go @@ -34,9 +34,10 @@ type OutputConfig struct { // ToolsConfig contains tool execution settings type ToolsConfig struct { - Enabled bool `yaml:"enabled"` - Whitelist ToolWhitelistConfig `yaml:"whitelist"` - Safety SafetyConfig `yaml:"safety"` + Enabled bool `yaml:"enabled"` + Whitelist ToolWhitelistConfig `yaml:"whitelist"` + Safety SafetyConfig `yaml:"safety"` + ExcludePaths []string `yaml:"exclude_paths"` } // ToolWhitelistConfig contains whitelisted commands and patterns @@ -76,7 +77,7 @@ func DefaultConfig() *Config { Enabled: true, Whitelist: ToolWhitelistConfig{ Commands: []string{ - "ls", "pwd", "echo", "cat", "head", "tail", + "ls", "pwd", "echo", "grep", "find", "wc", "sort", "uniq", }, Patterns: []string{ @@ -89,6 +90,10 @@ func DefaultConfig() *Config { Safety: SafetyConfig{ RequireApproval: true, }, + ExcludePaths: []string{ + ".infer/", + ".infer/*", + }, }, Compact: CompactConfig{ OutputDir: ".infer", diff --git a/internal/container/container.go b/internal/container/container.go index 371d8c15..0aabd112 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -55,7 +55,7 @@ func (c *ServiceContainer) initializeDomainServices() { c.config.Gateway.APIKey, ) - c.fileService = services.NewLocalFileService() + c.fileService = services.NewLocalFileService(c.config) if c.config.Tools.Enabled { c.toolService = services.NewLLMToolService(c.config, c.fileService) diff --git a/internal/services/file.go b/internal/services/file.go index 67d0843b..c223095d 100644 --- a/internal/services/file.go +++ b/internal/services/file.go @@ -9,6 +9,7 @@ import ( "sort" "strings" + "github.com/inference-gateway/cli/config" "github.com/inference-gateway/cli/internal/domain" ) @@ -18,11 +19,13 @@ type LocalFileService struct { excludeExts map[string]bool maxFileSize int64 maxDepth int + config *config.Config } // NewLocalFileService creates a new local file service -func NewLocalFileService() *LocalFileService { +func NewLocalFileService(cfg *config.Config) *LocalFileService { return &LocalFileService{ + config: cfg, excludeDirs: map[string]bool{ ".git": true, ".github": true, @@ -207,6 +210,10 @@ func (s *LocalFileService) ValidateFile(path string) error { return fmt.Errorf("file path cannot be empty") } + if s.isPathExcluded(path) { + return fmt.Errorf("file is excluded: %s", path) + } + absPath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("failed to resolve file path: %w", err) @@ -237,6 +244,46 @@ func (s *LocalFileService) ValidateFile(path string) error { return nil } +// isPathExcluded checks if a file path should be excluded based on configuration +func (s *LocalFileService) isPathExcluded(path string) bool { + if s.config == nil { + return false + } + + cleanPath := filepath.Clean(path) + + normalizedPath := filepath.ToSlash(cleanPath) + + for _, excludePattern := range s.config.Tools.ExcludePaths { + cleanPattern := filepath.Clean(excludePattern) + normalizedPattern := filepath.ToSlash(cleanPattern) + + if normalizedPath == normalizedPattern { + return true + } + + if strings.HasSuffix(normalizedPattern, "/*") { + dirPattern := strings.TrimSuffix(normalizedPattern, "/*") + if strings.HasPrefix(normalizedPath, dirPattern+"/") || normalizedPath == dirPattern { + return true + } + } + + if strings.HasSuffix(normalizedPattern, "/") { + dirPattern := strings.TrimSuffix(normalizedPattern, "/") + if strings.HasPrefix(normalizedPath, dirPattern+"/") || normalizedPath == dirPattern { + return true + } + } + + if strings.HasPrefix(normalizedPath, normalizedPattern) { + return true + } + } + + return false +} + func (s *LocalFileService) GetFileInfo(path string) (domain.FileInfo, error) { absPath, err := filepath.Abs(path) if err != nil { diff --git a/internal/services/tool.go b/internal/services/tool.go index 29ae753f..4bf651ff 100644 --- a/internal/services/tool.go +++ b/internal/services/tool.go @@ -204,8 +204,7 @@ func (s *LLMToolService) executeRead(filePath string, startLine, endLine int) (* } if err != nil { - result.Error = err.Error() - return result, nil + return nil, err } result.Content = content @@ -308,7 +307,7 @@ func (s *LLMToolService) executeReadTool(args map[string]interface{}) (string, e result, err := s.executeRead(filePath, startLine, endLine) if err != nil { - return "", fmt.Errorf("file read failed: %w", err) + return "", err } if format == "json" {