Files
clide/lib/kernel/src/toolchain.dart
T
jpmschweitzerandClaude Opus 4.7 70ce6c270e fix untrusted-workspace RCE in dugite git resolution (T-98)
Drop the workspaceRoot parameter from resolveToolchainPaths /
Toolchain.resolvePaths entirely. The old code resolved
\`<workspaceRoot>/native/dugite/bin/git\` as the git binary before
falling back to PATH — a malicious repo could commit an executable
at that path and clide would run it on the first auto-fired
git.status (which fires automatically on workspace open).

Dugite now resolves against trusted locations only:
1. CLIDE_DUGITE_DIR env var (dev override).
2. <exe-parent>/dugite/bin/git (production bundle).
3. <exe-parent>/lib/dugite/bin/git (alternate bundle layout).

Test plants `native/dugite/bin/git` in a temp workspace and asserts
the resolved git path is NOT inside the workspace.

Callers updated (8 sites): main.dart, backend_entry.dart twice,
test_app.dart three times (compute now wraps a no-arg call), plus
five test fixtures. backend.dart's now-vestigial hintRoot left in
the struct for cleanup under T-99.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:56:02 +02:00

84 lines
2.2 KiB
Dart

/// Centralized binary resolution for external tools.
///
/// Resolution runs in a background isolate via [Toolchain.resolvePaths]
/// to avoid blocking the merged UI/platform thread on macOS. The result
/// is applied on the main thread via [Toolchain.applyResolved].
///
/// The Flutter-free data types ([ResolvedPaths], [ToolchainView]) and
/// the isolate-side resolver ([resolveToolchainPaths]) live in
/// `toolchain_paths.dart` and are re-exported here for convenience.
library;
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'toolchain_paths.dart';
export 'toolchain_paths.dart';
class Toolchain extends ChangeNotifier implements ToolchainView {
String? _git;
String? _pql;
String? _tmux;
String? _shell;
Map<String, String>? _gitEnv;
bool _resolved = false;
@override
String get git => _git ?? 'git';
@override
String get pql => _pql ?? 'pql';
@override
String get tmux => _tmux ?? 'tmux';
@override
String get shell => _shell ?? '/bin/bash';
/// Extra environment variables for git (e.g. GIT_EXEC_PATH for dugite).
@override
Map<String, String>? get gitEnv => _gitEnv;
@override
bool get resolved => _resolved;
@override
bool get allOk => _resolved && missing.isEmpty;
@override
List<String> get missing => [
if (_git == null) 'git',
if (_pql == null) 'pql',
if (_tmux == null) 'tmux',
];
/// Returns a Future that completes when resolution finishes.
Future<void> waitForResolution() {
if (_resolved) return Future.value();
final c = Completer<void>();
void listener() {
if (_resolved) {
removeListener(listener);
if (!c.isCompleted) c.complete();
}
}
addListener(listener);
return c.future;
}
/// Apply paths resolved in a background isolate.
void applyResolved(ResolvedPaths p) {
_git = p.git;
_pql = p.pql;
_tmux = p.tmux;
_shell = p.shell;
_gitEnv = p.gitEnv;
_resolved = true;
notifyListeners();
}
/// Pure function — runs in a background isolate. All file I/O happens
/// here, off the main thread. Delegates to the Flutter-free
/// [resolveToolchainPaths]. Takes no workspace argument: see T-98.
static ResolvedPaths resolvePaths() => resolveToolchainPaths();
}