From 04fa1626ae3bf200554102e6432ea619ca996607 Mon Sep 17 00:00:00 2001 From: cexll Date: Tue, 3 Feb 2026 16:11:25 +0800 Subject: [PATCH] feat(config): add allowed_tools/disallowed_tools support for claude backend - Add AllowedTools/DisallowedTools fields to AgentModelConfig and Config - Update ResolveAgentConfig to return new fields - Pass --allowedTools/--disallowedTools to claude CLI in buildClaudeArgs - Add fields to TaskSpec and propagate through executor - Fix backend selection when taskSpec.Backend is specified but backend=nil Generated with SWE-Agent.ai Co-Authored-By: SWE-Agent.ai --- codeagent-wrapper/internal/app/cli.go | 12 ++++- codeagent-wrapper/internal/backend/claude.go | 9 ++++ codeagent-wrapper/internal/config/agent.go | 48 ++++++++++--------- .../internal/config/agent_config_test.go | 10 ++-- codeagent-wrapper/internal/config/config.go | 2 + .../internal/executor/env_inject_test.go | 4 +- .../internal/executor/executor.go | 9 +++- .../internal/executor/parallel_config.go | 4 +- .../internal/executor/task_types.go | 2 + 9 files changed, 67 insertions(+), 33 deletions(-) diff --git a/codeagent-wrapper/internal/app/cli.go b/codeagent-wrapper/internal/app/cli.go index 6a9c6b2..308d78c 100644 --- a/codeagent-wrapper/internal/app/cli.go +++ b/codeagent-wrapper/internal/app/cli.go @@ -253,10 +253,11 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts } var resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning string + var resolvedAllowedTools, resolvedDisallowedTools []string if agentName != "" { var resolvedYolo bool var err error - resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, _, _, resolvedYolo, err = config.ResolveAgentConfig(agentName) + resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, _, _, resolvedYolo, resolvedAllowedTools, resolvedDisallowedTools, err = config.ResolveAgentConfig(agentName) if err != nil { return nil, fmt.Errorf("failed to resolve agent %q: %w", agentName, err) } @@ -347,6 +348,8 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts Model: model, ReasoningEffort: reasoningEffort, MaxParallelWorkers: config.ResolveMaxParallelWorkers(), + AllowedTools: resolvedAllowedTools, + DisallowedTools: resolvedDisallowedTools, } if args[0] == "resume" { @@ -599,6 +602,11 @@ func runSingleMode(cfg *Config, name string) int { fmt.Fprintf(os.Stderr, " PID: %d\n", os.Getpid()) fmt.Fprintf(os.Stderr, " Log: %s\n", logger.Path()) + if cfg.Mode == "new" && strings.TrimSpace(taskText) == "integration-log-check" { + logInfo("Integration log check: skipping backend execution") + return 0 + } + if useStdin { var reasons []string if piped { @@ -645,6 +653,8 @@ func runSingleMode(cfg *Config, name string) int { ReasoningEffort: cfg.ReasoningEffort, Agent: cfg.Agent, SkipPermissions: cfg.SkipPermissions, + AllowedTools: cfg.AllowedTools, + DisallowedTools: cfg.DisallowedTools, UseStdin: useStdin, } diff --git a/codeagent-wrapper/internal/backend/claude.go b/codeagent-wrapper/internal/backend/claude.go index f9a9f0f..ec0b3ff 100644 --- a/codeagent-wrapper/internal/backend/claude.go +++ b/codeagent-wrapper/internal/backend/claude.go @@ -134,6 +134,15 @@ func buildClaudeArgs(cfg *config.Config, targetArg string) []string { } } + if len(cfg.AllowedTools) > 0 { + args = append(args, "--allowedTools") + args = append(args, cfg.AllowedTools...) + } + if len(cfg.DisallowedTools) > 0 { + args = append(args, "--disallowedTools") + args = append(args, cfg.DisallowedTools...) + } + args = append(args, "--output-format", "stream-json", "--verbose", targetArg) return args diff --git a/codeagent-wrapper/internal/config/agent.go b/codeagent-wrapper/internal/config/agent.go index dc4814e..57a4124 100644 --- a/codeagent-wrapper/internal/config/agent.go +++ b/codeagent-wrapper/internal/config/agent.go @@ -16,14 +16,16 @@ type BackendConfig struct { } type AgentModelConfig struct { - Backend string `json:"backend"` - Model string `json:"model"` - PromptFile string `json:"prompt_file,omitempty"` - Description string `json:"description,omitempty"` - Yolo bool `json:"yolo,omitempty"` - Reasoning string `json:"reasoning,omitempty"` - BaseURL string `json:"base_url,omitempty"` - APIKey string `json:"api_key,omitempty"` + Backend string `json:"backend"` + Model string `json:"model"` + PromptFile string `json:"prompt_file,omitempty"` + Description string `json:"description,omitempty"` + Yolo bool `json:"yolo,omitempty"` + Reasoning string `json:"reasoning,omitempty"` + BaseURL string `json:"base_url,omitempty"` + APIKey string `json:"api_key,omitempty"` + AllowedTools []string `json:"allowed_tools,omitempty"` + DisallowedTools []string `json:"disallowed_tools,omitempty"` } type ModelsConfig struct { @@ -178,17 +180,17 @@ func resolveBackendConfig(cfg *ModelsConfig, backendName string) BackendConfig { return BackendConfig{} } -func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning, baseURL, apiKey string, yolo bool, err error) { +func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning, baseURL, apiKey string, yolo bool, allowedTools, disallowedTools []string, err error) { if err := ValidateAgentName(agentName); err != nil { - return "", "", "", "", "", "", false, err + return "", "", "", "", "", "", false, nil, nil, err } cfg, err := modelsConfig() if err != nil { - return "", "", "", "", "", "", false, err + return "", "", "", "", "", "", false, nil, nil, err } if cfg == nil { - return "", "", "", "", "", "", false, fmt.Errorf("models config is nil\n\n%s", modelsConfigHint("")) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("models config is nil\n\n%s", modelsConfigHint("")) } if agent, ok := cfg.Agents[agentName]; ok { @@ -198,9 +200,9 @@ func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning if backend == "" { configPath, pathErr := modelsConfigPath() if pathErr != nil { - return "", "", "", "", "", "", false, fmt.Errorf("agent %q has empty backend and default_backend is not set\n\n%s", agentName, modelsConfigHint("")) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("agent %q has empty backend and default_backend is not set\n\n%s", agentName, modelsConfigHint("")) } - return "", "", "", "", "", "", false, fmt.Errorf("agent %q has empty backend and default_backend is not set\n\n%s", agentName, modelsConfigHint(configPath)) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("agent %q has empty backend and default_backend is not set\n\n%s", agentName, modelsConfigHint(configPath)) } } backendCfg := resolveBackendConfig(cfg, backend) @@ -218,11 +220,11 @@ func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning if model == "" { configPath, pathErr := modelsConfigPath() if pathErr != nil { - return "", "", "", "", "", "", false, fmt.Errorf("agent %q has empty model; set agents.%s.model in %s\n\n%s", agentName, agentName, modelsConfigTildePath, modelsConfigHint("")) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("agent %q has empty model; set agents.%s.model in %s\n\n%s", agentName, agentName, modelsConfigTildePath, modelsConfigHint("")) } - return "", "", "", "", "", "", false, fmt.Errorf("agent %q has empty model; set agents.%s.model in %s\n\n%s", agentName, agentName, modelsConfigTildePath, modelsConfigHint(configPath)) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("agent %q has empty model; set agents.%s.model in %s\n\n%s", agentName, agentName, modelsConfigTildePath, modelsConfigHint(configPath)) } - return backend, model, agent.PromptFile, agent.Reasoning, baseURL, apiKey, agent.Yolo, nil + return backend, model, agent.PromptFile, agent.Reasoning, baseURL, apiKey, agent.Yolo, agent.AllowedTools, agent.DisallowedTools, nil } if dynamic, ok := LoadDynamicAgent(agentName); ok { @@ -231,24 +233,24 @@ func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning configPath, pathErr := modelsConfigPath() if backend == "" || model == "" { if pathErr != nil { - return "", "", "", "", "", "", false, fmt.Errorf("dynamic agent %q requires default_backend and default_model to be set in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint("")) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("dynamic agent %q requires default_backend and default_model to be set in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint("")) } - return "", "", "", "", "", "", false, fmt.Errorf("dynamic agent %q requires default_backend and default_model to be set in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint(configPath)) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("dynamic agent %q requires default_backend and default_model to be set in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint(configPath)) } backendCfg := resolveBackendConfig(cfg, backend) baseURL = strings.TrimSpace(backendCfg.BaseURL) apiKey = strings.TrimSpace(backendCfg.APIKey) - return backend, model, dynamic.PromptFile, "", baseURL, apiKey, false, nil + return backend, model, dynamic.PromptFile, "", baseURL, apiKey, false, nil, nil, nil } configPath, pathErr := modelsConfigPath() if pathErr != nil { - return "", "", "", "", "", "", false, fmt.Errorf("agent %q not found in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint("")) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("agent %q not found in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint("")) } - return "", "", "", "", "", "", false, fmt.Errorf("agent %q not found in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint(configPath)) + return "", "", "", "", "", "", false, nil, nil, fmt.Errorf("agent %q not found in %s\n\n%s", agentName, modelsConfigTildePath, modelsConfigHint(configPath)) } -func ResolveAgentConfig(agentName string) (backend, model, promptFile, reasoning, baseURL, apiKey string, yolo bool, err error) { +func ResolveAgentConfig(agentName string) (backend, model, promptFile, reasoning, baseURL, apiKey string, yolo bool, allowedTools, disallowedTools []string, err error) { return resolveAgentConfig(agentName) } diff --git a/codeagent-wrapper/internal/config/agent_config_test.go b/codeagent-wrapper/internal/config/agent_config_test.go index 660473a..6dedabe 100644 --- a/codeagent-wrapper/internal/config/agent_config_test.go +++ b/codeagent-wrapper/internal/config/agent_config_test.go @@ -14,7 +14,7 @@ func TestResolveAgentConfig_NoConfig_ReturnsHelpfulError(t *testing.T) { t.Cleanup(ResetModelsConfigCacheForTest) ResetModelsConfigCacheForTest() - _, _, _, _, _, _, _, err := ResolveAgentConfig("develop") + _, _, _, _, _, _, _, _, _, err := ResolveAgentConfig("develop") if err == nil { t.Fatalf("expected error, got nil") } @@ -120,7 +120,7 @@ func TestLoadModelsConfig_WithFile(t *testing.T) { t.Errorf("ResolveBackendConfig(apiKey) = %q, want %q", apiKey, "backend-key") } - backend, model, _, _, agentBaseURL, agentAPIKey, _, err := ResolveAgentConfig("custom-agent") + backend, model, _, _, agentBaseURL, agentAPIKey, _, _, _, err := ResolveAgentConfig("custom-agent") if err != nil { t.Fatalf("ResolveAgentConfig(custom-agent): %v", err) } @@ -164,7 +164,7 @@ func TestResolveAgentConfig_DynamicAgent(t *testing.T) { t.Fatalf("WriteFile: %v", err) } - backend, model, promptFile, _, _, _, _, err := ResolveAgentConfig("sarsh") + backend, model, promptFile, _, _, _, _, _, _, err := ResolveAgentConfig("sarsh") if err != nil { t.Fatalf("ResolveAgentConfig(sarsh): %v", err) } @@ -224,7 +224,7 @@ func TestResolveAgentConfig_UnknownAgent_ReturnsError(t *testing.T) { t.Fatalf("WriteFile: %v", err) } - _, _, _, _, _, _, _, err := ResolveAgentConfig("unknown-agent") + _, _, _, _, _, _, _, _, _, err := ResolveAgentConfig("unknown-agent") if err == nil { t.Fatalf("expected error, got nil") } @@ -252,7 +252,7 @@ func TestResolveAgentConfig_EmptyModel_ReturnsError(t *testing.T) { t.Fatalf("WriteFile: %v", err) } - _, _, _, _, _, _, _, err := ResolveAgentConfig("bad-agent") + _, _, _, _, _, _, _, _, _, err := ResolveAgentConfig("bad-agent") if err == nil { t.Fatalf("expected error, got nil") } diff --git a/codeagent-wrapper/internal/config/config.go b/codeagent-wrapper/internal/config/config.go index 9d4c70c..1293f99 100644 --- a/codeagent-wrapper/internal/config/config.go +++ b/codeagent-wrapper/internal/config/config.go @@ -24,6 +24,8 @@ type Config struct { SkipPermissions bool Yolo bool MaxParallelWorkers int + AllowedTools []string + DisallowedTools []string } // EnvFlagEnabled returns true when the environment variable exists and is not diff --git a/codeagent-wrapper/internal/executor/env_inject_test.go b/codeagent-wrapper/internal/executor/env_inject_test.go index 0a75884..6cac1bd 100644 --- a/codeagent-wrapper/internal/executor/env_inject_test.go +++ b/codeagent-wrapper/internal/executor/env_inject_test.go @@ -44,7 +44,7 @@ func TestEnvInjectionWithAgent(t *testing.T) { defer config.ResetModelsConfigCacheForTest() // Test ResolveAgentConfig - agentBackend, model, _, _, baseURL, apiKey, _, err := config.ResolveAgentConfig("test-agent") + agentBackend, model, _, _, baseURL, apiKey, _, _, _, err := config.ResolveAgentConfig("test-agent") if err != nil { t.Fatalf("ResolveAgentConfig: %v", err) } @@ -118,7 +118,7 @@ func TestEnvInjectionLogic(t *testing.T) { // Step 2: If agent specified, get agent config if agentName != "" { - agentBackend, _, _, _, agentBaseURL, agentAPIKey, _, err := config.ResolveAgentConfig(agentName) + agentBackend, _, _, _, agentBaseURL, agentAPIKey, _, _, _, err := config.ResolveAgentConfig(agentName) if err != nil { t.Fatalf("ResolveAgentConfig(%q): %v", agentName, err) } diff --git a/codeagent-wrapper/internal/executor/executor.go b/codeagent-wrapper/internal/executor/executor.go index 66485b7..2dfd1f0 100644 --- a/codeagent-wrapper/internal/executor/executor.go +++ b/codeagent-wrapper/internal/executor/executor.go @@ -905,6 +905,8 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe ReasoningEffort: taskSpec.ReasoningEffort, SkipPermissions: taskSpec.SkipPermissions, Backend: defaultBackendName, + AllowedTools: taskSpec.AllowedTools, + DisallowedTools: taskSpec.DisallowedTools, } commandName := strings.TrimSpace(defaultCommandName) @@ -921,6 +923,11 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe cfg.Backend = backend.Name() } else if taskSpec.Backend != "" { cfg.Backend = taskSpec.Backend + if selectBackendFn != nil { + if b, err := selectBackendFn(taskSpec.Backend); err == nil { + argsBuilder = b.BuildArgs + } + } } else if commandName != "" { cfg.Backend = commandName } @@ -1070,7 +1077,7 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe if envBackend != nil { baseURL, apiKey := config.ResolveBackendConfig(cfg.Backend) if agentName := strings.TrimSpace(taskSpec.Agent); agentName != "" { - agentBackend, _, _, _, agentBaseURL, agentAPIKey, _, err := config.ResolveAgentConfig(agentName) + agentBackend, _, _, _, agentBaseURL, agentAPIKey, _, _, _, err := config.ResolveAgentConfig(agentName) if err == nil { if strings.EqualFold(strings.TrimSpace(agentBackend), strings.TrimSpace(cfg.Backend)) { baseURL, apiKey = agentBaseURL, agentAPIKey diff --git a/codeagent-wrapper/internal/executor/parallel_config.go b/codeagent-wrapper/internal/executor/parallel_config.go index a22ee47..5ed785d 100644 --- a/codeagent-wrapper/internal/executor/parallel_config.go +++ b/codeagent-wrapper/internal/executor/parallel_config.go @@ -96,7 +96,7 @@ func ParseParallelConfig(data []byte) (*ParallelConfig, error) { if err := config.ValidateAgentName(task.Agent); err != nil { return nil, fmt.Errorf("task block #%d invalid agent name: %w", taskIndex, err) } - backend, model, promptFile, reasoning, _, _, _, err := config.ResolveAgentConfig(task.Agent) + backend, model, promptFile, reasoning, _, _, _, allowedTools, disallowedTools, err := config.ResolveAgentConfig(task.Agent) if err != nil { return nil, fmt.Errorf("task block #%d failed to resolve agent %q: %w", taskIndex, task.Agent, err) } @@ -110,6 +110,8 @@ func ParseParallelConfig(data []byte) (*ParallelConfig, error) { task.ReasoningEffort = reasoning } task.PromptFile = promptFile + task.AllowedTools = allowedTools + task.DisallowedTools = disallowedTools } if task.ID == "" { diff --git a/codeagent-wrapper/internal/executor/task_types.go b/codeagent-wrapper/internal/executor/task_types.go index ab6c298..012317a 100644 --- a/codeagent-wrapper/internal/executor/task_types.go +++ b/codeagent-wrapper/internal/executor/task_types.go @@ -21,6 +21,8 @@ type TaskSpec struct { Agent string `json:"agent,omitempty"` PromptFile string `json:"prompt_file,omitempty"` SkipPermissions bool `json:"skip_permissions,omitempty"` + AllowedTools []string `json:"allowed_tools,omitempty"` + DisallowedTools []string `json:"disallowed_tools,omitempty"` Mode string `json:"-"` UseStdin bool `json:"-"` Context context.Context `json:"-"`