generateRunCodeSandbox function

String generateRunCodeSandbox()

Generates the _runCodeSandbox method.

Implementation

String generateRunCodeSandbox() {
  return '''
  Future<String?> _runCodeSandbox(String userCode, int timeoutSeconds) async {
    io.Process? process;
    io.Directory? tempDir;
    try {
      final wrapper = _buildJsWrapper(userCode);

      // Security: Use unpredictable directory name with timestamp + random suffix
      final timestamp = DateTime.now().millisecondsSinceEpoch;
      final random = math.Random.secure();
      final suffix = List.generate(8, (_) => random.nextInt(16).toRadixString(16)).join();
      tempDir = await io.Directory.systemTemp.createTemp('mcp_sandbox_\${timestamp}_\${suffix}_');
      final scriptFile = io.File('\${tempDir.path}/sandbox.js');

      // Set restrictive permissions (owner read/write only)
      if (io.Platform.isLinux || io.Platform.isMacOS) {
        await scriptFile.create(recursive: true);
        await io.Process.run('chmod', ['700', tempDir.path]);
        await io.Process.run('chmod', ['600', scriptFile.path]);
      }

      await scriptFile.writeAsString(wrapper);

      process = await io.Process.start(
        'node',
        [
          '--max-old-space-size=64',
          '--no-addons',
          '--frozen-intrinsics',
          scriptFile.path,
        ],
      );
    } catch (e) {
      await tempDir?.delete(recursive: true);
      throw StateError('Code mode requires Node.js to be installed');
    }

    try {
      final resultCompleter = Completer<String?>();
      final errorCompleter = Completer<String>();

      process.stdout
          .transform(utf8.decoder)
          .transform(const LineSplitter())
          .listen((line) {
        if (line.trim().isEmpty) return;

        try {
          final msg = jsonDecode(line) as Map<String, dynamic>;
          final type = msg['type'] as String?;

          if (type == 'call') {
            final callId = msg['callId'] as String;
            final toolName = msg['tool'] as String;
            final args = (msg['args'] as Map<String, dynamic>?) ?? <String, dynamic>{};

            _dispatchCodeModeToolCall(toolName, args).then((resultJson) {
              process?.stdin.writeln(jsonEncode({
                'type': 'result',
                'callId': callId,
                'data': resultJson,
              }));
            }).catchError((e, st) {
              if (_logErrors) {
                io.stderr.writeln('[easy_api] _dispatchCodeModeToolCall(\$toolName): \$e');
                io.stderr.writeln(st);
                io.stderr.flush();  // fire-and-forget; callback is not async
              }
              process?.stdin.writeln(jsonEncode({
                'type': 'result',
                'callId': callId,
                'data': null,
                'error': 'An error occurred while processing the request.',
              }));
            });
          } else if (type == 'done') {
            final result = msg['result'];
            if (result == null) {
              resultCompleter.complete(null);
            } else if (result is String) {
              resultCompleter.complete(result);
            } else {
              resultCompleter.complete(jsonEncode(result));
            }
          } else if (type == 'error') {
            errorCompleter.complete(msg['message'] as String? ?? 'Unknown error');
          }
        } catch (_) {
          // Ignore non-JSON lines
        }
      });

      // Wait for result, error, or timeout
      final timeoutFuture = Future.delayed(
        Duration(seconds: timeoutSeconds),
        () => throw StateError('Code execution timed out after \$timeoutSeconds seconds'),
      );

      final result = await Future.any<String?>([
        resultCompleter.future,
        errorCompleter.future.then((e) => throw StateError('Code execution error: \$e')),
        timeoutFuture,
      ]);

      return result;
    } finally {
      // Graceful shutdown: process is guaranteed to be non-null here
      // (otherwise process.stdout would have thrown earlier)
      final proc = process;
      final dir = tempDir;
      try {
        // First, check if process already exited naturally
        await proc.exitCode.timeout(
          const Duration(milliseconds: 100),
        );
      } catch (_) {
        // Process still running, begin graceful shutdown
        proc.kill(io.ProcessSignal.sigterm);
        try {
          // Wait up to 2 seconds for graceful shutdown
          await proc.exitCode.timeout(
            const Duration(seconds: 2),
            onTimeout: () {
              // Process didn't exit, force kill
              proc.kill(io.ProcessSignal.sigkill);
              return -1;
            },
          );
        } catch (_) {
          // Error during exit code wait - attempt force kill as fallback
          proc.kill(io.ProcessSignal.sigkill);
        }
      }
      await dir.delete(recursive: true);
    }
  }''';
}