(ctx context.Context, toolConfig *latest.ScriptShellToolConfig, toolCall tools.ToolCall)
| 207 | } |
| 208 | |
| 209 | func (t *ScriptToolSet) execute(ctx context.Context, toolConfig *latest.ScriptShellToolConfig, toolCall tools.ToolCall) (*tools.ToolCallResult, error) { |
| 210 | var params map[string]any |
| 211 | if toolCall.Function.Arguments != "" { |
| 212 | if err := json.Unmarshal([]byte(toolCall.Function.Arguments), ¶ms); err != nil { |
| 213 | return nil, fmt.Errorf("invalid arguments: %w", err) |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | // working_dir accepts ~, $VAR, ${VAR} and ${env.VAR} like every other |
| 218 | // working_dir field (issue #2615). |
| 219 | workingDir := path.ExpandWorkingDir("script shell working_dir", toolConfig.WorkingDir) |
| 220 | |
| 221 | // Stamp the script_shell call shape onto the active span. Cmd |
| 222 | // ships unconditionally for the same reason as shell.RunShell — |
| 223 | // see that comment for the redact-at-collector guidance. |
| 224 | if span := trace.SpanFromContext(ctx); span.IsRecording() { |
| 225 | span.SetAttributes( |
| 226 | attribute.String("cagent.tool.script_shell.tool_name", toolCall.Function.Name), |
| 227 | attribute.String("cagent.tool.script_shell.cmd", toolConfig.Cmd), |
| 228 | attribute.String("cagent.tool.script_shell.cwd", cmp.Or(workingDir, ".")), |
| 229 | ) |
| 230 | } |
| 231 | |
| 232 | shell, argsPrefix := shellpath.DetectShell() |
| 233 | |
| 234 | cmd := exec.CommandContext(ctx, shell, append(argsPrefix, toolConfig.Cmd)...) |
| 235 | cmd.Dir = workingDir |
| 236 | // Per-call clone: appending onto t.env would mutate the shared |
| 237 | // backing array under concurrent calls. Expand nil to os.Environ() |
| 238 | // so a nil t.env still inherits the parent env (a non-nil empty |
| 239 | // slice would strip it). |
| 240 | base := t.env |
| 241 | if base == nil { |
| 242 | base = os.Environ() |
| 243 | } |
| 244 | envCopy := make([]string, len(base), len(base)+len(toolConfig.Env)+len(toolConfig.Args)) |
| 245 | copy(envCopy, base) |
| 246 | // Per-tool env overrides the toolset-level env (exec.Cmd dedupes with |
| 247 | // last-wins). Only the plain ${env.X} form is expanded; $X and ${X} |
| 248 | // stay literal because env values may legitimately contain $ (issue |
| 249 | // #2615). |
| 250 | for _, key := range slices.Sorted(maps.Keys(toolConfig.Env)) { |
| 251 | envCopy = append(envCopy, key+"="+path.ExpandEnvRefs(toolConfig.Env[key])) |
| 252 | } |
| 253 | for key, value := range params { |
| 254 | if value == nil { |
| 255 | continue |
| 256 | } |
| 257 | // Only forward arguments declared in the tool's schema. The |
| 258 | // LLM may hallucinate extra keys (e.g. LD_PRELOAD, PATH); |
| 259 | // without this filter they would land verbatim in the |
| 260 | // spawned process's environment. |
| 261 | if _, declared := toolConfig.Args[key]; !declared { |
| 262 | continue |
| 263 | } |
| 264 | valueStr := fmt.Sprintf("%v", value) |
| 265 | // A NUL byte mid-string silently truncates env entries at the |
| 266 | // execve boundary; refuse rather than spawn a process with a |
no test coverage detected