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 <noreply@swe-agent.ai>
This commit is contained in:
cexll
2026-02-03 16:11:25 +08:00
parent c0f61d5cc2
commit 04fa1626ae
9 changed files with 67 additions and 33 deletions

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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 == "" {

View File

@@ -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:"-"`