executeCommand function

Future<BashToolOutput> executeCommand(
  1. BashToolInput input, {
  2. ShellExecOptions options = const ShellExecOptions(),
  3. SandboxConfig sandbox = const SandboxConfig(),
})

Execute a shell command with full BashTool semantics.

Implementation

Future<BashToolOutput> executeCommand(
  BashToolInput input, {
  ShellExecOptions options = const ShellExecOptions(),
  SandboxConfig sandbox = const SandboxConfig(),
}) async {
  // 1. Security validation
  final securityResult = validateCommandSecurity(input.command);
  if (!securityResult.passed) {
    return BashToolOutput(
      stdout: '',
      stderr: 'Security violation: ${securityResult.violations.join('; ')}',
      exitCode: -1,
    );
  }

  // 2. Path safety check
  final pathCheck = checkPathSafety(input.command);
  if (pathCheck.isDangerous) {
    return BashToolOutput(
      stdout: '',
      stderr: pathCheck.reason ?? 'Dangerous path operation',
      exitCode: -1,
    );
  }

  // 3. Determine timeout
  final timeout = input.timeoutMs != null
      ? Duration(milliseconds: input.timeoutMs!)
      : options.timeout;

  // 4. Build environment
  final env = <String, String>{
    ...?options.environment,
    'NEOMAGECODE': '1', // Side-channel hint
    'TERM': 'dumb', // Disable terminal features
  };

  // 5. Execute
  final shell = Platform.isWindows
      ? 'cmd.exe'
      : Platform.environment['SHELL'] ?? '/bin/sh';
  final shellArgs = Platform.isWindows
      ? ['/c', input.command]
      : ['-c', input.command];

  try {
    final process = await Process.start(
      shell,
      shellArgs,
      workingDirectory: options.workingDirectory,
      environment: env,
    );

    final stdoutBuf = StringBuffer();
    final stderrBuf = StringBuffer();
    var totalBytes = 0;
    var interrupted = false;

    // Capture output with size limiting
    final stdoutSub = process.stdout.transform(utf8.decoder).listen((chunk) {
      totalBytes += chunk.length;
      if (totalBytes <= options.maxOutputBytes) {
        stdoutBuf.write(chunk);
        options.onProgress?.call(chunk);
      }
    });

    final stderrSub = process.stderr.transform(utf8.decoder).listen((chunk) {
      if (options.mergeStderr) {
        totalBytes += chunk.length;
        if (totalBytes <= options.maxOutputBytes) {
          stdoutBuf.write(chunk);
        }
      } else {
        stderrBuf.write(chunk);
      }
    });

    // Wait with timeout
    int exitCode;
    try {
      exitCode = await process.exitCode.timeout(timeout);
    } on TimeoutException {
      interrupted = true;
      process.kill(ProcessSignal.sigterm);
      await Future.delayed(const Duration(seconds: 2));
      process.kill(ProcessSignal.sigkill);
      exitCode = await process.exitCode.timeout(
        const Duration(seconds: 3),
        onTimeout: () => -1,
      );
    }

    await stdoutSub.cancel();
    await stderrSub.cancel();

    var stdout = stdoutBuf.toString();
    final stderr = stderrBuf.toString();

    // Process output
    stdout = stripEmptyLines(stdout);

    if (totalBytes > options.maxOutputBytes) {
      stdout = truncateOutput(stdout, maxLength: options.maxOutputBytes);
    }

    // Check for image output
    final isImage = isImageOutput(stdout);

    // Semantic exit code
    final interpretation = interpretExitCode(input.command, exitCode);

    // Silent command check
    final noOutput = stdout.trim().isEmpty && isSilentCommand(input.command);

    return BashToolOutput(
      stdout: stdout,
      stderr: stderr,
      exitCode: exitCode,
      interrupted: interrupted,
      isImage: isImage,
      returnCodeInterpretation: interpretation,
      noOutputExpected: noOutput,
    );
  } catch (e) {
    return BashToolOutput(stdout: '', stderr: e.toString(), exitCode: -1);
  }
}