From e109e43d996de3800a1a6afca500ef14cfeba3b1 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 6 Apr 2026 09:39:16 +0800 Subject: [PATCH] Bundle go-core helper with macOS app and drop external CLI fallback --- lib/runtime/aris_llm_chat_client.dart | 15 +-- lib/runtime/embedded_agent_launch_policy.dart | 15 +++ .../go_agent_core_desktop_transport.dart | 11 +- lib/runtime/go_core.dart | 102 ++++++++++-------- .../go_gateway_runtime_desktop_client.dart | 11 +- .../go_multi_agent_mount_desktop_client.dart | 11 +- .../go_runtime_dispatch_desktop_client.dart | 11 +- macos/Runner.xcodeproj/project.pbxproj | 21 ++++ scripts/embed-go-core-helper.sh | 33 ++++++ scripts/package-flutter-mac-app.sh | 13 +-- test/runtime/aris_llm_chat_client_suite.dart | 12 ++- .../embedded_agent_launch_policy_test.dart | 25 +++++ test/runtime/go_core_suite.dart | 64 +++++++++-- test/runtime/multi_agent_mounts_suite.dart | 1 - 14 files changed, 248 insertions(+), 97 deletions(-) create mode 100755 scripts/embed-go-core-helper.sh diff --git a/lib/runtime/aris_llm_chat_client.dart b/lib/runtime/aris_llm_chat_client.dart index 09aba415..ef1673ae 100644 --- a/lib/runtime/aris_llm_chat_client.dart +++ b/lib/runtime/aris_llm_chat_client.dart @@ -88,17 +88,18 @@ class ArisLlmChatClient { required Map environment, required Map arguments, }) async { - if (shouldBlockEmbeddedAgentLaunch( - isAppleHost: Platform.isIOS || Platform.isMacOS, - )) { - throw UnsupportedError( - 'App Store builds do not allow launching the bundled Go core process.', - ); - } final launch = await _bridgeLocator.locate(); if (launch == null) { throw StateError('Go core is unavailable.'); } + if (shouldBlockGoCoreLaunch( + launch, + isAppleHost: Platform.isIOS || Platform.isMacOS, + )) { + throw UnsupportedError( + 'App Store builds only allow the bundled Go core helper inside the app bundle.', + ); + } final process = await _processStarter( launch.executable, diff --git a/lib/runtime/embedded_agent_launch_policy.dart b/lib/runtime/embedded_agent_launch_policy.dart index f7131369..b587df97 100644 --- a/lib/runtime/embedded_agent_launch_policy.dart +++ b/lib/runtime/embedded_agent_launch_policy.dart @@ -1,4 +1,5 @@ import '../app/app_store_policy.dart'; +import 'go_core.dart'; bool shouldBlockEmbeddedAgentLaunch({ required bool isAppleHost, @@ -9,3 +10,17 @@ bool shouldBlockEmbeddedAgentLaunch({ enabled: enabled, ); } + +bool shouldBlockGoCoreLaunch( + GoCoreLaunch launch, { + required bool isAppleHost, + bool? enabled, +}) { + if (!shouldApplyAppleAppStorePolicy( + isAppleHost: isAppleHost, + enabled: enabled, + )) { + return false; + } + return launch.source != GoCoreLaunchSource.bundledHelper; +} diff --git a/lib/runtime/go_agent_core_desktop_transport.dart b/lib/runtime/go_agent_core_desktop_transport.dart index f4d14f62..1035a0f3 100644 --- a/lib/runtime/go_agent_core_desktop_transport.dart +++ b/lib/runtime/go_agent_core_desktop_transport.dart @@ -195,15 +195,16 @@ class GoAgentCoreDesktopTransport implements GoAgentCoreClient { } Future _startLocalProcess() async { - if (shouldBlockEmbeddedAgentLaunch( - isAppleHost: Platform.isIOS || Platform.isMacOS, - )) { - return null; - } final launch = await _goCoreLocator.locate(); if (launch == null) { return null; } + if (shouldBlockGoCoreLaunch( + launch, + isAppleHost: Platform.isIOS || Platform.isMacOS, + )) { + return null; + } final reservedSocket = await ServerSocket.bind( InternetAddress.loopbackIPv4, 0, diff --git a/lib/runtime/go_core.dart b/lib/runtime/go_core.dart index d9571f08..9688fae3 100644 --- a/lib/runtime/go_core.dart +++ b/lib/runtime/go_core.dart @@ -1,13 +1,20 @@ import 'dart:io'; +enum GoCoreLaunchSource { + bundledHelper, + buildArtifact, +} + class GoCoreLaunch { const GoCoreLaunch({ required this.executable, + required this.source, this.arguments = const [], this.workingDirectory, }); final String executable; + final GoCoreLaunchSource source; final List arguments; final String? workingDirectory; } @@ -33,39 +40,12 @@ class GoCoreLocator { return bundled; } - final override = - (Platform.environment['XWORKMATE_GO_CORE_BIN'] ?? - Platform.environment['GO_CORE_BIN'] ?? - '') - .trim(); - if (override.isNotEmpty && await _binaryExists(override)) { - return GoCoreLaunch(executable: override); - } - - for (final candidate in ['xworkmate-go-core', 'go-core']) { - if (await _binaryExists(candidate)) { - return GoCoreLaunch(executable: candidate); - } - } - - final root = (_workspaceRoot ?? Directory.current.path).trim(); - if (root.isNotEmpty) { - for (final path in [ - '$root/go/bin/xworkmate-go-core', - '$root/go/bin/go-core', - '$root/build/bin/xworkmate-go-core', - ]) { - if (await File(path).exists()) { - return GoCoreLaunch(executable: path); - } - } - - final packageDirectory = Directory('$root/go/go_core'); - if (await packageDirectory.exists() && await _binaryExists('go')) { + for (final root in _candidateRoots()) { + final path = '$root/build/bin/xworkmate-go-core'; + if (await _binaryExists(path)) { return GoCoreLaunch( - executable: 'go', - arguments: const ['run', '.'], - workingDirectory: packageDirectory.path, + executable: path, + source: GoCoreLaunchSource.buildArtifact, ); } } @@ -94,25 +74,55 @@ class GoCoreLocator { return null; } final bundledPath = '${contentsDirectory.path}/Helpers/xworkmate-go-core'; - if (await File(bundledPath).exists()) { - return GoCoreLaunch(executable: bundledPath); + if (await _binaryExists(bundledPath)) { + return GoCoreLaunch( + executable: bundledPath, + source: GoCoreLaunchSource.bundledHelper, + ); } return null; } - Future _binaryExists(String command) async { - final resolver = _binaryExistsResolver; - if (resolver != null) { - return resolver(command); + List _candidateRoots() { + final roots = {}; + final explicitRoot = _workspaceRoot?.trim() ?? ''; + if (explicitRoot.isNotEmpty) { + roots.add(explicitRoot); + roots.addAll(_ancestorPaths(Directory(explicitRoot))); } - if (command.contains(Platform.pathSeparator)) { - return File(command).exists(); + + final currentPath = Directory.current.path.trim(); + if (currentPath.isNotEmpty) { + roots.add(currentPath); + roots.addAll(_ancestorPaths(Directory(currentPath))); } - final check = await Process.run( - Platform.isWindows ? 'where' : 'which', - [command], - runInShell: true, - ); - return check.exitCode == 0 && '${check.stdout}'.trim().isNotEmpty; + + final resolvedExecutable = + (_resolvedExecutableResolver?.call() ?? Platform.resolvedExecutable) + .trim(); + if (resolvedExecutable.isNotEmpty) { + final executableDirectory = File(resolvedExecutable).parent; + roots.add(executableDirectory.path); + roots.addAll(_ancestorPaths(executableDirectory)); + } + + return roots.where((path) => path.trim().isNotEmpty).toList(growable: false); } + + List _ancestorPaths(Directory start) { + final ancestors = []; + var current = start.absolute; + while (true) { + final parent = current.parent; + if (parent.path == current.path) { + break; + } + ancestors.add(parent.path); + current = parent; + } + return ancestors; + } + + Future _binaryExists(String command) async => + (_binaryExistsResolver?.call(command)) ?? File(command).exists(); } diff --git a/lib/runtime/go_gateway_runtime_desktop_client.dart b/lib/runtime/go_gateway_runtime_desktop_client.dart index d7b003f8..e370cde3 100644 --- a/lib/runtime/go_gateway_runtime_desktop_client.dart +++ b/lib/runtime/go_gateway_runtime_desktop_client.dart @@ -291,15 +291,16 @@ class GoGatewayRuntimeDesktopClient implements GatewayRuntimeSessionClient { } Future _startLocalProcess() async { - if (shouldBlockEmbeddedAgentLaunch( - isAppleHost: Platform.isIOS || Platform.isMacOS, - )) { - return null; - } final launch = await _goCoreLocator.locate(); if (launch == null) { return null; } + if (shouldBlockGoCoreLaunch( + launch, + isAppleHost: Platform.isIOS || Platform.isMacOS, + )) { + return null; + } final reservedSocket = await ServerSocket.bind( InternetAddress.loopbackIPv4, 0, diff --git a/lib/runtime/go_multi_agent_mount_desktop_client.dart b/lib/runtime/go_multi_agent_mount_desktop_client.dart index a21d9933..9972871a 100644 --- a/lib/runtime/go_multi_agent_mount_desktop_client.dart +++ b/lib/runtime/go_multi_agent_mount_desktop_client.dart @@ -132,15 +132,16 @@ class GoMultiAgentMountDesktopClient implements MultiAgentMountResolver { } Future _startLocalProcess() async { - if (shouldBlockEmbeddedAgentLaunch( - isAppleHost: Platform.isIOS || Platform.isMacOS, - )) { - return null; - } final launch = await _goCoreLocator.locate(); if (launch == null) { return null; } + if (shouldBlockGoCoreLaunch( + launch, + isAppleHost: Platform.isIOS || Platform.isMacOS, + )) { + return null; + } final reservedSocket = await ServerSocket.bind( InternetAddress.loopbackIPv4, 0, diff --git a/lib/runtime/go_runtime_dispatch_desktop_client.dart b/lib/runtime/go_runtime_dispatch_desktop_client.dart index dcf615b6..08e3ac14 100644 --- a/lib/runtime/go_runtime_dispatch_desktop_client.dart +++ b/lib/runtime/go_runtime_dispatch_desktop_client.dart @@ -152,15 +152,16 @@ class GoRuntimeDispatchDesktopClient implements RuntimeDispatchResolver { } Future _startLocalProcess() async { - if (shouldBlockEmbeddedAgentLaunch( - isAppleHost: Platform.isIOS || Platform.isMacOS, - )) { - return null; - } final launch = await _goCoreLocator.locate(); if (launch == null) { return null; } + if (shouldBlockGoCoreLaunch( + launch, + isAppleHost: Platform.isIOS || Platform.isMacOS, + )) { + return null; + } final reservedSocket = await ServerSocket.bind( InternetAddress.loopbackIPv4, 0, diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 3d5996de..a4a12401 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -248,6 +248,7 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + A1B2C3084F0A000100000001 /* Embed Bundled Go Core Helper */, 93B26977D4D2EC7AFAB54C8E /* [CP] Embed Pods Frameworks */, A1B2C3074F0A000100000001 /* Generate Missing Framework dSYMs */, ); @@ -432,6 +433,26 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + A1B2C3084F0A000100000001 /* Embed Bundled Go Core Helper */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Embed Bundled Go Core Helper"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/bash \"${PROJECT_DIR}/../scripts/embed-go-core-helper.sh\" \"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n"; + showEnvVarsInLog = 0; + }; A1B2C3074F0A000100000001 /* Generate Missing Framework dSYMs */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/scripts/embed-go-core-helper.sh b/scripts/embed-go-core-helper.sh new file mode 100755 index 00000000..30fc60a0 --- /dev/null +++ b/scripts/embed-go-core-helper.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +APP_BUNDLE_PATH="${1:-${APP_BUNDLE_PATH:-}}" +BRIDGE_BINARY_NAME="${BRIDGE_BINARY_NAME:-xworkmate-go-core}" +BRIDGE_BUILD_PATH="${ROOT_DIR}/build/bin/${BRIDGE_BINARY_NAME}" + +if [[ -z "$APP_BUNDLE_PATH" ]]; then + echo "Missing app bundle path for embedded go-core helper" >&2 + exit 1 +fi + +if [[ ! -d "$APP_BUNDLE_PATH" ]]; then + echo "App bundle does not exist: $APP_BUNDLE_PATH" >&2 + exit 1 +fi + +HELPERS_DIR="$APP_BUNDLE_PATH/Contents/Helpers" +HELPER_PATH="$HELPERS_DIR/$BRIDGE_BINARY_NAME" + +bash "$ROOT_DIR/scripts/build-go-core.sh" + +mkdir -p "$HELPERS_DIR" +ditto "$BRIDGE_BUILD_PATH" "$HELPER_PATH" +chmod +x "$HELPER_PATH" + +SIGN_IDENTITY="${EXPANDED_CODE_SIGN_IDENTITY:-${CODE_SIGN_IDENTITY:--}}" +if [[ -n "$SIGN_IDENTITY" ]]; then + codesign --force --sign "$SIGN_IDENTITY" --timestamp=none "$HELPER_PATH" +fi + +echo "Embedded go-core helper: $HELPER_PATH" diff --git a/scripts/package-flutter-mac-app.sh b/scripts/package-flutter-mac-app.sh index e351ea45..229ce334 100755 --- a/scripts/package-flutter-mac-app.sh +++ b/scripts/package-flutter-mac-app.sh @@ -9,8 +9,6 @@ APP_NAME="${APP_NAME:-XWorkmate}" BUILD_MODE="${BUILD_MODE:-release}" APP_STORE_DEFINE="${APP_STORE_DEFINE:---dart-define=XWORKMATE_APP_STORE=${XWORKMATE_APP_STORE:-true}}" PRODUCTS_DIR_NAME="$(tr '[:lower:]' '[:upper:]' <<< "${BUILD_MODE:0:1}")${BUILD_MODE:1}" -BRIDGE_BINARY_NAME="${BRIDGE_BINARY_NAME:-xworkmate-go-core}" -BRIDGE_BUILD_PATH="${ROOT_DIR}/build/bin/${BRIDGE_BINARY_NAME}" FLUTTER_BUILD_STATE_DIR="${ROOT_DIR}/.dart_tool/flutter_build" MACOS_BUILD_DIR="${ROOT_DIR}/build/macos" NATIVE_ASSETS_DIR="${ROOT_DIR}/build/native_assets" @@ -35,14 +33,8 @@ fi BUILD_APP_PATH="$APP_DIR/build/macos/Build/Products/$PRODUCTS_DIR_NAME/$APP_NAME.app" DIST_APP_PATH="$DIST_DIR/$APP_NAME.app" DIST_DMG_PATH="$DIST_DIR/$APP_NAME-$APP_VERSION.dmg" -HELPERS_DIR="$DIST_APP_PATH/Contents/Helpers" -HELPER_PATH="$HELPERS_DIR/$BRIDGE_BINARY_NAME" - mkdir -p "$DIST_DIR" -echo "Building bundled Go core..." -bash "$ROOT_DIR/scripts/build-go-core.sh" - echo "Building $APP_NAME $APP_VERSION ($APP_BUILD) for macOS..." # Flutter caches native-asset installation state under .dart_tool/flutter_build, # but Xcode consumes the copied frameworks from build/native_assets/macos. @@ -74,13 +66,10 @@ bash "$ROOT_DIR/scripts/check-apple-export-compliance.sh" "$BUILD_APP_PATH" rm -rf "$DIST_APP_PATH" "$DIST_DMG_PATH" ditto "$BUILD_APP_PATH" "$DIST_APP_PATH" -mkdir -p "$HELPERS_DIR" -ditto "$BRIDGE_BUILD_PATH" "$HELPER_PATH" -chmod +x "$HELPER_PATH" +bash "$ROOT_DIR/scripts/embed-go-core-helper.sh" "$DIST_APP_PATH" echo "Re-signing bundled helper and app..." SIGN_IDENTITY="${XWORKMATE_SIGN_IDENTITY:--}" -codesign --force --sign "$SIGN_IDENTITY" --timestamp=none "$HELPER_PATH" codesign --force --deep --sign "$SIGN_IDENTITY" --preserve-metadata=entitlements,requirements,flags,runtime --timestamp=none "$DIST_APP_PATH" echo "Packaging DMG..." diff --git a/test/runtime/aris_llm_chat_client_suite.dart b/test/runtime/aris_llm_chat_client_suite.dart index 7b98af76..53e80df5 100644 --- a/test/runtime/aris_llm_chat_client_suite.dart +++ b/test/runtime/aris_llm_chat_client_suite.dart @@ -113,11 +113,17 @@ void main() { } GoCoreLocator _fixedLocator() { + final appRoot = Directory('${Directory.systemTemp.path}/aris-llm-chat-app'); + final helpersDir = Directory('${appRoot.path}/XWorkmate.app/Contents/Helpers'); + helpersDir.createSync(recursive: true); + final helper = File('${helpersDir.path}/xworkmate-go-core'); + if (!helper.existsSync()) { + helper.writeAsStringSync('#!/bin/sh\nexit 0\n'); + Process.runSync('chmod', ['+x', helper.path]); + } return GoCoreLocator( - binaryExistsResolver: (_) async => true, - workspaceRoot: Directory.systemTemp.path, resolvedExecutableResolver: () => - '${Directory.systemTemp.path}/XWorkmate.app/Contents/MacOS/XWorkmate', + '${appRoot.path}/XWorkmate.app/Contents/MacOS/XWorkmate', ); } diff --git a/test/runtime/embedded_agent_launch_policy_test.dart b/test/runtime/embedded_agent_launch_policy_test.dart index f85346b7..b3cd56b2 100644 --- a/test/runtime/embedded_agent_launch_policy_test.dart +++ b/test/runtime/embedded_agent_launch_policy_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/runtime/embedded_agent_launch_policy.dart'; +import 'package:xworkmate/runtime/go_core.dart'; void main() { test('apple app store policy blocks embedded agent launches', () { @@ -12,4 +13,28 @@ void main() { isFalse, ); }); + + test('apple app store policy allows only bundled go core helpers', () { + const bundled = GoCoreLaunch( + executable: '/Applications/XWorkmate.app/Contents/Helpers/xworkmate-go-core', + source: GoCoreLaunchSource.bundledHelper, + ); + const buildArtifact = GoCoreLaunch( + executable: '/tmp/build/bin/xworkmate-go-core', + source: GoCoreLaunchSource.buildArtifact, + ); + + expect( + shouldBlockGoCoreLaunch(bundled, isAppleHost: true, enabled: true), + isFalse, + ); + expect( + shouldBlockGoCoreLaunch(buildArtifact, isAppleHost: true, enabled: true), + isTrue, + ); + expect( + shouldBlockGoCoreLaunch(buildArtifact, isAppleHost: false, enabled: true), + isFalse, + ); + }); } diff --git a/test/runtime/go_core_suite.dart b/test/runtime/go_core_suite.dart index 795b0177..53410bc9 100644 --- a/test/runtime/go_core_suite.dart +++ b/test/runtime/go_core_suite.dart @@ -37,13 +37,14 @@ void main() { expect(launch, isNotNull); expect(launch!.executable, helperFile.path); + expect(launch.source, GoCoreLaunchSource.bundledHelper); expect(launch.arguments, isEmpty); expect(launch.workingDirectory, isNull); }, ); test( - 'GoCoreLocator falls back to go run in the local bridge package', + 'GoCoreLocator resolves the local build artifact from the workspace root', () async { final tempDirectory = await Directory.systemTemp.createTemp( 'xworkmate-go-core-', @@ -53,21 +54,68 @@ void main() { await tempDirectory.delete(recursive: true); } }); - await Directory( - '${tempDirectory.path}/go/go_core', - ).create(recursive: true); + final bridgeFile = File('${tempDirectory.path}/build/bin/xworkmate-go-core'); + await bridgeFile.parent.create(recursive: true); + await bridgeFile.writeAsString('#!/bin/sh\nexit 0\n'); + await Process.run('chmod', ['+x', bridgeFile.path]); final locator = GoCoreLocator( workspaceRoot: tempDirectory.path, - binaryExistsResolver: (command) async => command == 'go', ); final launch = await locator.locate(); expect(launch, isNotNull); - expect(launch!.executable, 'go'); - expect(launch.arguments, const ['run', '.']); - expect(launch.workingDirectory, '${tempDirectory.path}/go/go_core'); + expect(launch!.executable, bridgeFile.path); + expect(launch.source, GoCoreLaunchSource.buildArtifact); + expect(launch.arguments, isEmpty); + expect(launch.workingDirectory, isNull); + }, + ); + + test( + 'GoCoreLocator resolves build-root bridge binaries from the executable ancestry when cwd is outside the repo', + () async { + final tempDirectory = await Directory.systemTemp.createTemp( + 'xworkmate-go-core-build-root-', + ); + final outsideDirectory = await Directory.systemTemp.createTemp( + 'xworkmate-go-core-outside-', + ); + final originalCurrentDirectory = Directory.current; + addTearDown(() async { + Directory.current = originalCurrentDirectory; + if (await tempDirectory.exists()) { + await tempDirectory.delete(recursive: true); + } + if (await outsideDirectory.exists()) { + await outsideDirectory.delete(recursive: true); + } + }); + + final bridgeFile = File('${tempDirectory.path}/build/bin/xworkmate-go-core'); + await bridgeFile.parent.create(recursive: true); + await bridgeFile.writeAsString('#!/bin/sh\nexit 0\n'); + await Process.run('chmod', ['+x', bridgeFile.path]); + + final executablePath = + '${tempDirectory.path}/build/macos/Build/Products/Debug/XWorkmate.app/Contents/MacOS/XWorkmate'; + await File(executablePath).parent.create(recursive: true); + await File(executablePath).writeAsString(''); + + Directory.current = outsideDirectory; + + final locator = GoCoreLocator( + resolvedExecutableResolver: () => executablePath, + ); + + final launch = await locator.locate(); + + expect(launch, isNotNull); + expect(launch!.executable, bridgeFile.path); + expect(launch.source, GoCoreLaunchSource.buildArtifact); + expect(launch.arguments, isEmpty); + expect(launch.workingDirectory, isNull); }, ); } diff --git a/test/runtime/multi_agent_mounts_suite.dart b/test/runtime/multi_agent_mounts_suite.dart index c6469de9..04e154cd 100644 --- a/test/runtime/multi_agent_mounts_suite.dart +++ b/test/runtime/multi_agent_mounts_suite.dart @@ -89,7 +89,6 @@ void main() { await Process.run('chmod', ['+x', helper.path]); final locator = GoCoreLocator( workspaceRoot: tempDir.path, - binaryExistsResolver: (_) async => false, resolvedExecutableResolver: () => '${tempDir.path}/XWorkmate.app/Contents/MacOS/XWorkmate', );