| 228 | } |
| 229 | |
| 230 | func (h *shellHandler) RunShell(ctx context.Context, params RunShellArgs) (*tools.ToolCallResult, error) { |
| 231 | if strings.TrimSpace(params.Cmd) == "" { |
| 232 | return tools.ResultError(`Error: missing or empty "cmd" parameter. Pass the shell command as {"cmd": "..."}.`), nil |
| 233 | } |
| 234 | |
| 235 | timeout := h.timeout |
| 236 | if params.Timeout > 0 { |
| 237 | timeout = time.Duration(params.Timeout) * time.Second |
| 238 | } |
| 239 | |
| 240 | timeoutCtx, cancel := context.WithTimeout(ctx, timeout) |
| 241 | defer cancel() |
| 242 | |
| 243 | cwd := h.resolveWorkDir(params.Cwd) |
| 244 | |
| 245 | // Stamp the call shape (cmd, cwd, timeout) onto the active span. |
| 246 | // Cmd ships unconditionally — it's the main signal of what the |
| 247 | // agent actually did, and gating it on chat-content capture loses |
| 248 | // too much debug value. Drop or hash `cagent.tool.shell.cmd` at |
| 249 | // the OTel collector if commands routinely carry secrets. |
| 250 | if span := trace.SpanFromContext(ctx); span.IsRecording() { |
| 251 | span.SetAttributes( |
| 252 | attribute.String("cagent.tool.shell.cmd", params.Cmd), |
| 253 | attribute.Float64("cagent.tool.shell.timeout_seconds", timeout.Seconds()), |
| 254 | attribute.String("cagent.tool.shell.cwd", cwd), |
| 255 | ) |
| 256 | } |
| 257 | |
| 258 | slog.DebugContext(ctx, "Executing native shell command", "command", params.Cmd, "cwd", cwd) |
| 259 | |
| 260 | return h.runNativeCommand(timeoutCtx, ctx, params.Cmd, cwd, timeout), nil |
| 261 | } |
| 262 | |
| 263 | // waitDelayAfterShellExit caps how long cmd.Wait() blocks on stdout/stderr |
| 264 | // copy goroutines after the direct shell child has exited. |