diff --git a/agent/sandbox/claude/command.go b/agent/sandbox/claude/command.go index 0c5a8fe1..252c5472 100644 --- a/agent/sandbox/claude/command.go +++ b/agent/sandbox/claude/command.go @@ -53,8 +53,8 @@ When working with GitHub and a token is provided: // Only keys listed here are passed through; everything else is ignored. var claudeArgWhitelist = map[string]string{ "max_turns": "--max-turns", // Maximum conversation turns - "disallowed_tools": "--disallowed-tools", // Comma-separated tool blacklist (e.g. "WebSearch,WebFetch") - "allowed_tools": "--allowedTools", // Comma-separated tool whitelist (e.g. "Bash,Read,Write") + "disallowed_tools": "--disallowed-tools", // Comma-separated tool blacklist (e.g. "WebSearch,WebFetch") + "allowed_tools": "--allowedTools", // Comma-separated tool whitelist (e.g. "Bash,Read,Write") } // BuildCommand builds the Claude CLI command and environment variables diff --git a/agent/sandbox/claude/command_test.go b/agent/sandbox/claude/command_test.go index a35876d5..9797fd5b 100644 --- a/agent/sandbox/claude/command_test.go +++ b/agent/sandbox/claude/command_test.go @@ -118,8 +118,9 @@ func TestBuildProxyConfig(t *testing.T) { configStr := string(configJSON) // Proxy config uses simple format + // BuildAPIURL adds /v1 prefix for hosts that don't end with "/" assert.Contains(t, configStr, "backend") - assert.Contains(t, configStr, "https://api.example.com/chat/completions") + assert.Contains(t, configStr, "https://api.example.com/v1/chat/completions") assert.Contains(t, configStr, "api_key") assert.Contains(t, configStr, "key123") assert.Contains(t, configStr, "model") diff --git a/agent/sandbox/claude/executor.go b/agent/sandbox/claude/executor.go index 53599f48..1b03aaa5 100644 --- a/agent/sandbox/claude/executor.go +++ b/agent/sandbox/claude/executor.go @@ -474,7 +474,6 @@ func (e *Executor) prepareAttachments(ctx context.Context, messages []agentConte // Process each content part var textParts []string - modified := false for _, item := range parts { m, ok := item.(map[string]interface{}) @@ -504,7 +503,6 @@ func (e *Executor) prepareAttachments(ctx context.Context, messages []agentConte if !isWrapper { // Not an attachment URL, keep as text reference textParts = append(textParts, fmt.Sprintf("[Image: %s]", url)) - modified = true continue } @@ -513,13 +511,11 @@ func (e *Executor) prepareAttachments(ctx context.Context, messages []agentConte if err != nil { log.Printf("[sandbox] Warning: failed to resolve image attachment %s: %v", fileID, err) textParts = append(textParts, "[Attached image: failed to load]") - modified = true continue } textParts = append(textParts, ref) hasAttachments = true - modified = true case "file": fileData, _ := m["file"].(map[string]interface{}) @@ -535,7 +531,6 @@ func (e *Executor) prepareAttachments(ctx context.Context, messages []agentConte uploaderName, fileID, isWrapper := attachment.Parse(url) if !isWrapper { textParts = append(textParts, fmt.Sprintf("[File: %s]", url)) - modified = true continue } @@ -543,13 +538,11 @@ func (e *Executor) prepareAttachments(ctx context.Context, messages []agentConte if err != nil { log.Printf("[sandbox] Warning: failed to resolve file attachment %s: %v", fileID, err) textParts = append(textParts, "[Attached file: failed to load]") - modified = true continue } textParts = append(textParts, ref) hasAttachments = true - modified = true default: // Keep other types as-is (shouldn't happen normally) @@ -557,7 +550,11 @@ func (e *Executor) prepareAttachments(ctx context.Context, messages []agentConte } } - if modified && len(textParts) > 0 { + // Merge text parts into a single string when the original content was + // a multimodal array ([]interface{} / []ContentPart). This is needed + // even when only "text" parts are present so that downstream code + // (BuildInputJSONL, etc.) always sees a plain string. + if len(textParts) > 0 { newMsg := result[i] newMsg.Content = strings.Join(textParts, "\n\n") result[i] = newMsg