fix(release): harden apple app store distribution

This commit is contained in:
Haitao Pan 2026-03-23 17:28:54 +08:00
parent 2b534ce845
commit 144428f054
15 changed files with 541 additions and 36 deletions

View File

@ -6,6 +6,7 @@ FLUTTER ?= flutter
PNPM ?= pnpm
DART ?= dart
DEVICE ?= macos
APP_STORE_DART_DEFINE ?= --dart-define=XWORKMATE_APP_STORE=true
.PHONY: help deps analyze test check format run build-linux build-macos build-ios-sim package-deb package-rpm package-linux package-mac install-mac clean build-aris-bridge render-release-docs
@ -36,10 +37,10 @@ build-linux: ## Build the Linux app in release mode
$(FLUTTER) build linux --release
build-macos: ## Build the macOS app in release mode
$(FLUTTER) build macos --release
$(FLUTTER) build macos --release $(APP_STORE_DART_DEFINE)
build-ios-sim: ## Build the iOS app for the simulator
$(FLUTTER) build ios --simulator
$(FLUTTER) build ios --simulator $(APP_STORE_DART_DEFINE)
build-aris-bridge: ## Build the ARIS Go bridge helper
bash scripts/build-aris-bridge.sh
@ -54,10 +55,10 @@ package-linux: ## Create both Linux packages
bash scripts/package-linux.sh
package-mac: ## Create the macOS .app and DMG
bash scripts/package-flutter-mac-app.sh
XWORKMATE_APP_STORE=true bash scripts/package-flutter-mac-app.sh
install-mac: ## Package and install the macOS app into /Applications
bash scripts/package-flutter-mac-app.sh
XWORKMATE_APP_STORE=true bash scripts/package-flutter-mac-app.sh
bash scripts/install-flutter-mac-dmg.sh
clean: ## Remove generated artifacts

View File

@ -14,6 +14,7 @@
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
790F5BD2C520842BBA31950C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63E65220C02DE80AF75C238E /* Pods_Runner.framework */; };
7F0C4AAE0C8458F9E652862D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29ABF973925162A04B6C3BE4 /* Pods_RunnerTests.framework */; };
8E6F4A7B31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8E6F4A7A31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -56,6 +57,7 @@
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
8E6F4A7A31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@ -158,6 +160,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
8E6F4A7A31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
@ -262,6 +265,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8E6F4A7B31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy in Resources */,
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,

View File

@ -26,6 +26,8 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>XWorkmate uses your local network only when you explicitly connect to a user-configured OpenClaw Gateway on the same network.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'app_metadata.dart';
import 'app_capabilities.dart';
import 'app_store_policy.dart';
import 'ui_feature_manifest.dart';
import '../i18n/app_language.dart';
import '../models/app_models.dart';
@ -203,7 +204,12 @@ class AppController extends ChangeNotifier {
String? get bootstrapError => _bootstrapError;
UiFeatureAccess featuresFor(UiFeaturePlatform platform) {
return _uiFeatureManifest.forPlatform(platform);
final manifest = applyAppleAppStorePolicy(
_uiFeatureManifest,
hostPlatform: platform,
isAppleHost: Platform.isIOS || Platform.isMacOS,
);
return manifest.forPlatform(platform);
}
RuntimeCoordinator get runtimeCoordinator => _runtimeCoordinator;
@ -323,6 +329,11 @@ class AppController extends ChangeNotifier {
availableSingleAgentProviders.isNotEmpty;
bool _canUseSingleAgentProvider(SingleAgentProvider provider) {
if (!allowsAppStoreExternalSingleAgentProviders(
isAppleHost: Platform.isIOS || Platform.isMacOS,
)) {
return false;
}
final override = _availableSingleAgentProvidersOverride;
if (override != null) {
return provider != SingleAgentProvider.auto &&
@ -460,8 +471,11 @@ class AppController extends ChangeNotifier {
SingleAgentProvider singleAgentProviderForSession(String sessionKey) {
final normalizedSessionKey = _normalizedAssistantSessionKey(sessionKey);
return _assistantThreadRecords[normalizedSessionKey]?.singleAgentProvider ??
SingleAgentProvider.auto;
return sanitizeAppStoreSingleAgentProvider(
_assistantThreadRecords[normalizedSessionKey]?.singleAgentProvider ??
SingleAgentProvider.auto,
isAppleHost: Platform.isIOS || Platform.isMacOS,
);
}
SingleAgentProvider get currentSingleAgentProvider =>
@ -549,7 +563,11 @@ class AppController extends ChangeNotifier {
singleAgentModelDisplayLabelForSession(currentSessionKey);
List<SingleAgentProvider> get singleAgentProviderOptions =>
SingleAgentProvider.values;
allowsAppStoreExternalSingleAgentProviders(
isAppleHost: Platform.isIOS || Platform.isMacOS,
)
? SingleAgentProvider.values
: const <SingleAgentProvider>[SingleAgentProvider.auto];
String singleAgentProviderLabelForSession(String sessionKey) {
return singleAgentProviderForSession(sessionKey).label;
@ -1688,12 +1706,16 @@ class AppController extends ChangeNotifier {
Future<void> setSingleAgentProvider(SingleAgentProvider provider) async {
final sessionKey = _normalizedAssistantSessionKey(currentSessionKey);
if (singleAgentProviderForSession(sessionKey) == provider) {
final sanitizedProvider = sanitizeAppStoreSingleAgentProvider(
provider,
isAppleHost: Platform.isIOS || Platform.isMacOS,
);
if (singleAgentProviderForSession(sessionKey) == sanitizedProvider) {
return;
}
_upsertAssistantThreadRecord(
sessionKey,
singleAgentProvider: provider,
singleAgentProvider: sanitizedProvider,
discoveredSkills: const <AssistantThreadSkillEntry>[],
importedSkills: const <AssistantThreadSkillEntry>[],
selectedSkillKeys: const <String>[],
@ -2606,6 +2628,7 @@ class AppController extends ChangeNotifier {
setActiveAppLanguage(settings.appLanguage);
await _desktopPlatformService.initialize(settings.linuxDesktop);
await _desktopPlatformService.setLaunchAtLogin(settings.launchAtLogin);
await _refreshResolvedCodexCliPath();
_registerCodexExternalProvider();
await _refreshAcpCapabilities(persistMountTargets: true);
if (_disposed) {
@ -2791,6 +2814,7 @@ class AppController extends ChangeNotifier {
}
if (previous.codexCliPath != current.codexCliPath ||
previous.codeAgentRuntimeMode != current.codeAgentRuntimeMode) {
await _refreshResolvedCodexCliPath();
_registerCodexExternalProvider();
}
if (previous.linuxDesktop.toJson().toString() !=
@ -4395,13 +4419,6 @@ class AppController extends ChangeNotifier {
capabilities = const GatewayAcpCapabilities.empty();
}
_acpCapabilities = capabilities;
_resolvedCodexCliPath =
capabilities.providers.contains(SingleAgentProvider.codex)
? appText(
'通过 Gateway ACP 能力协商检测到 Codex Provider',
'Detected Codex provider via Gateway ACP capability negotiation',
)
: null;
if (persistMountTargets && !_disposed) {
final currentConfig = settings.multiAgent;
final nextTargets = _mergeAcpCapabilitiesIntoMountTargets(
@ -4420,6 +4437,30 @@ class AppController extends ChangeNotifier {
_notifyIfActive();
}
Future<void> _refreshResolvedCodexCliPath() async {
if (effectiveCodeAgentRuntimeMode != CodeAgentRuntimeMode.externalCli) {
_resolvedCodexCliPath = null;
return;
}
final configuredPath = configuredCodexCliPath;
String? detectedPath;
if (configuredPath.isNotEmpty) {
try {
if (await File(configuredPath).exists()) {
detectedPath = configuredPath;
}
} catch (_) {
detectedPath = null;
}
}
detectedPath ??= await _runtimeCoordinator.codex.findCodexBinary();
if (_disposed) {
return;
}
_resolvedCodexCliPath = detectedPath;
}
List<ManagedMountTargetState> _mergeAcpCapabilitiesIntoMountTargets(
List<ManagedMountTargetState> current,
GatewayAcpCapabilities capabilities,

View File

@ -0,0 +1,156 @@
import '../runtime/runtime_models.dart';
import 'ui_feature_manifest.dart';
const bool kAppStoreDistribution = bool.fromEnvironment(
'XWORKMATE_APP_STORE',
defaultValue: false,
);
bool shouldApplyAppleAppStorePolicy({
required bool isAppleHost,
bool? enabled,
}) {
return (enabled ?? kAppStoreDistribution) && isAppleHost;
}
UiFeatureManifest applyAppleAppStorePolicy(
UiFeatureManifest manifest, {
required UiFeaturePlatform hostPlatform,
required bool isAppleHost,
bool? enabled,
}) {
if (!shouldApplyAppleAppStorePolicy(
isAppleHost: isAppleHost,
enabled: enabled,
)) {
return manifest;
}
var next = manifest;
final disabledPaths = <(UiFeaturePlatform, String, String)>[
(
hostPlatform,
'navigation',
_featureKeyLeaf(UiFeatureKeys.navigationAgents),
),
(
hostPlatform,
'navigation',
_featureKeyLeaf(UiFeatureKeys.navigationMcpServer),
),
(
hostPlatform,
'navigation',
_featureKeyLeaf(UiFeatureKeys.navigationClawHub),
),
(hostPlatform, 'workspace', _featureKeyLeaf(UiFeatureKeys.workspaceAgents)),
(
hostPlatform,
'workspace',
_featureKeyLeaf(UiFeatureKeys.workspaceMcpServer),
),
(
hostPlatform,
'workspace',
_featureKeyLeaf(UiFeatureKeys.workspaceClawHub),
),
(hostPlatform, 'settings', _featureKeyLeaf(UiFeatureKeys.settingsAgents)),
(
hostPlatform,
'settings',
_featureKeyLeaf(UiFeatureKeys.settingsExperimental),
),
(
hostPlatform,
'settings',
_featureKeyLeaf(UiFeatureKeys.settingsExperimentalCanvas),
),
(
hostPlatform,
'settings',
_featureKeyLeaf(UiFeatureKeys.settingsExperimentalBridge),
),
(
hostPlatform,
'settings',
_featureKeyLeaf(UiFeatureKeys.settingsExperimentalDebug),
),
];
if (hostPlatform == UiFeaturePlatform.mobile) {
disabledPaths.addAll(<(UiFeaturePlatform, String, String)>[
(
hostPlatform,
'assistant',
_featureKeyLeaf(UiFeatureKeys.assistantLocalGateway),
),
(
hostPlatform,
'assistant',
_featureKeyLeaf(UiFeatureKeys.assistantMultiAgent),
),
]);
}
if (hostPlatform == UiFeaturePlatform.desktop) {
disabledPaths.addAll(<(UiFeaturePlatform, String, String)>[
(
hostPlatform,
'assistant',
_featureKeyLeaf(UiFeatureKeys.assistantMultiAgent),
),
(
hostPlatform,
'assistant',
_featureKeyLeaf(UiFeatureKeys.assistantLocalRuntime),
),
]);
}
for (final (platform, module, feature) in disabledPaths) {
if (next.lookup(platform, module, feature) == null) {
continue;
}
next = next.copyWithFeature(
platform: platform,
module: module,
feature: feature,
enabled: false,
buildModes: const <UiFeatureBuildMode>{},
);
}
return next;
}
bool allowsAppStoreExternalSingleAgentProviders({
required bool isAppleHost,
bool? enabled,
}) {
return !shouldApplyAppleAppStorePolicy(
isAppleHost: isAppleHost,
enabled: enabled,
);
}
SingleAgentProvider sanitizeAppStoreSingleAgentProvider(
SingleAgentProvider provider, {
required bool isAppleHost,
bool? enabled,
}) {
if (!allowsAppStoreExternalSingleAgentProviders(
isAppleHost: isAppleHost,
enabled: enabled,
)) {
return SingleAgentProvider.auto;
}
return provider;
}
String _featureKeyLeaf(String keyPath) {
final segments = keyPath.split('.');
if (segments.isEmpty) {
throw StateError('Invalid feature key path: $keyPath');
}
return segments.last;
}

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/app_store_policy.dart';
import '../../app/ui_feature_manifest.dart';
import '../../app/workspace_navigation.dart';
import '../../i18n/app_language.dart';
@ -2482,12 +2483,130 @@ class _SettingsPageState extends State<SettingsPage> {
label: appText('包名', 'Package'),
value: controller.runtime.packageInfo.packageName,
),
if (kAppStoreDistribution) ...[
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: Text(
appText(
'当前构建启用了 App Store 分发策略Apple 渠道会隐藏实验入口,并禁用外部 CLI / 本地 Runtime 能力。',
'This build enables the App Store distribution policy: Apple storefront builds hide experimental surfaces and disable external CLI / local runtime capabilities.',
),
),
),
],
],
),
),
const SizedBox(height: 16),
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('隐私政策', 'Privacy Policy'),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
Text(
appText(
'说明本应用会保存哪些本地设置、哪些用户数据会按你的操作发送到外部网关或 LLM 端点,以及如何清除本地数据。',
'Explains which settings stay on-device, which user data is sent to your configured gateway or LLM endpoints, and how to clear local data.',
),
),
const SizedBox(height: 16),
FilledButton.tonalIcon(
key: const ValueKey('settings-open-privacy-policy'),
onPressed: () => _showPrivacyPolicyDialog(context),
icon: const Icon(Icons.privacy_tip_outlined),
label: Text(appText('查看隐私政策', 'View Privacy Policy')),
),
],
),
),
];
}
Future<void> _showPrivacyPolicyDialog(BuildContext context) {
final theme = Theme.of(context);
return showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(appText('隐私政策', 'Privacy Policy')),
content: SizedBox(
width: 560,
child: SingleChildScrollView(
child: Text(
appText(_privacyPolicyZh, _privacyPolicyEn),
style: theme.textTheme.bodyMedium,
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(appText('关闭', 'Close')),
),
],
);
},
);
}
static const String _privacyPolicyZh = '''
XWorkmate
1.
- 线稿
- TokenAPI Key 使 SharedPreferences
2.
- OpenClaw Gateway LLM API Endpoint
-
3.
- 广 SDK
- Token LLM API Token
4.
- OpenClaw GatewayLLM API Endpoint
-
5.
- -> /线
-
''';
static const String _privacyPolicyEn = '''
XWorkmate Privacy Policy
1. Local storage
- The app stores the settings, UI preferences, draft threads, and diagnostic state that you explicitly save on this device.
- Shared tokens, passwords, and API keys are stored in platform secure storage instead of plain SharedPreferences.
2. Data sent to external services
- Data is only sent when you explicitly connect, send a message, attach a file, or run a connection test against your configured OpenClaw Gateway or LLM API endpoint.
- Sent data can include prompts, conversation context, user-selected attachment paths and file contents, and the authentication headers required to complete the request.
3. What the app does not do
- It does not include advertising SDKs, cross-app tracking, or automatic workspace file reads without a user action.
- It does not upload your gateway passwords, shared tokens, or LLM API tokens to developer-operated services by default.
4. Third-party processing
- Your configured OpenClaw Gateway, LLM API endpoint, object storage, or other external services process the data you send under their own terms.
- You are responsible for confirming that those external services meet your compliance requirements.
5. Deletion and withdrawal
- You can clear local threads, remove local settings, and delete stored secrets from Settings.
- If you need data removed from an external service, you must request deletion from that external service directly.
''';
Future<void> _saveSettings(
AppController controller,
SettingsSnapshot snapshot,

View File

@ -124,6 +124,8 @@ class SettingsStore {
await _deleteDurableStateFile(settingsKey);
await _deleteDurableStateFile(assistantThreadsKey);
await _deleteLegacyBackupFile();
_lastRecoveryReport = const LegacyRecoveryReport();
_recoveryAttempted = true;
}
Future<List<SecretAuditEntry>> loadAuditTrail() async {
@ -365,13 +367,17 @@ class SettingsStore {
final results = <String>{};
final databasePath = await _resolveDatabasePath();
final fallbackRoot = await _fallbackDirectoryPathResolver?.call();
final defaultSupportRoot = await _defaultSupportDirectoryPathResolver
?.call();
final hasExplicitPaths =
_databasePathResolver != null || _fallbackDirectoryPathResolver != null;
String? defaultSupportRoot;
String? supportPath;
try {
supportPath = (await getApplicationSupportDirectory()).path;
} catch (_) {
supportPath = null;
if (!hasExplicitPaths) {
defaultSupportRoot = await _defaultSupportDirectoryPathResolver?.call();
try {
supportPath = (await getApplicationSupportDirectory()).path;
} catch (_) {
supportPath = null;
}
}
void addPath(String? path) {
@ -385,7 +391,6 @@ class SettingsStore {
if (databasePath != null && databasePath.trim().isNotEmpty) {
final directory = File(databasePath).parent.path;
addPath(directory);
addPath(Directory(directory).parent.path);
}
addPath(fallbackRoot);
addPath(fallbackRoot == null ? null : '$fallbackRoot/xworkmate');

View File

@ -26,6 +26,7 @@
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
8E6F4A7D31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8E6F4A7C31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
A96EF8FFA0E80B16252FE834 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5F4830709C3A67EC8096888 /* Pods_RunnerTests.framework */; };
F02922E20E15948F8CE5469F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2099D82E31DC5912EA477802 /* Pods_Runner.framework */; };
@ -73,6 +74,7 @@
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
8E6F4A7C31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Runner/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
@ -156,6 +158,7 @@
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
8E6F4A7C31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy */,
);
name = Resources;
path = ..;
@ -316,6 +319,7 @@
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
8E6F4A7D31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -7,6 +7,7 @@ PUBSPEC_PATH="$ROOT_DIR/pubspec.yaml"
DIST_DIR="$ROOT_DIR/dist"
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-aris-bridge}"
BRIDGE_BUILD_PATH="${ROOT_DIR}/build/bin/${BRIDGE_BINARY_NAME}"
@ -47,6 +48,7 @@ BUILD_ARGS=(
--build-number="$APP_BUILD"
--dart-define="XWORKMATE_DISPLAY_VERSION=$APP_VERSION"
--dart-define="XWORKMATE_BUILD_NUMBER=$APP_BUILD"
"$APP_STORE_DEFINE"
)
if [[ -f "$APP_DIR/.dart_tool/package_config.json" ]]; then

View File

@ -0,0 +1,71 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/app_store_policy.dart';
import 'package:xworkmate/app/ui_feature_manifest.dart';
import 'package:xworkmate/models/app_models.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
void main() {
test('apple app store policy disables restricted desktop surfaces', () {
final manifest = applyAppleAppStorePolicy(
UiFeatureManifest.fallback(),
hostPlatform: UiFeaturePlatform.desktop,
isAppleHost: true,
enabled: true,
);
final access = manifest.forPlatform(
UiFeaturePlatform.desktop,
buildMode: UiFeatureBuildMode.release,
);
expect(access.supportsDesktopRuntime, isFalse);
expect(access.supportsMultiAgent, isFalse);
expect(
access.allowedDestinations.contains(WorkspaceDestination.agents),
isFalse,
);
expect(
access.allowedDestinations.contains(WorkspaceDestination.clawHub),
isFalse,
);
expect(access.availableSettingsTabs.contains(SettingsTab.agents), isFalse);
});
test('apple app store policy disables local mobile assistant features', () {
final manifest = applyAppleAppStorePolicy(
UiFeatureManifest.fallback(),
hostPlatform: UiFeaturePlatform.mobile,
isAppleHost: true,
enabled: true,
);
final access = manifest.forPlatform(
UiFeaturePlatform.mobile,
buildMode: UiFeatureBuildMode.release,
);
expect(access.supportsLocalGateway, isFalse);
expect(access.supportsMultiAgent, isFalse);
expect(
access.availableExecutionTargets,
equals(<AssistantExecutionTarget>[AssistantExecutionTarget.remote]),
);
});
test('single-agent provider selection is forced to auto for app store', () {
expect(
sanitizeAppStoreSingleAgentProvider(
SingleAgentProvider.codex,
isAppleHost: true,
enabled: true,
),
SingleAgentProvider.auto,
);
expect(
sanitizeAppStoreSingleAgentProvider(
SingleAgentProvider.gemini,
isAppleHost: false,
enabled: true,
),
SingleAgentProvider.gemini,
);
});
}

View File

@ -28,6 +28,6 @@ void main() {
);
expect(find.text('OpenClaw Gateway'), findsOneWidget);
expect(find.text('Vault Server'), findsOneWidget);
expect(find.text('Vault Server'), findsNothing);
});
}

View File

@ -17,20 +17,28 @@ void main() {
'SettingsPage AI Gateway draft/save/apply flow persists edited fields through local actions',
(WidgetTester tester) async {
late _AiGatewaySettingsTestController controller;
late Directory testRoot;
await tester.runAsync(() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final testRoot =
'${Directory.systemTemp.path}/xworkmate-widget-tests-${DateTime.now().microsecondsSinceEpoch}';
testRoot = await Directory.systemTemp.createTemp(
'xworkmate-widget-tests-',
);
controller = _AiGatewaySettingsTestController(
store: SecureConfigStore(
enableSecureStorage: false,
databasePathResolver: () async => '$testRoot/settings.sqlite3',
fallbackDirectoryPathResolver: () async => testRoot,
databasePathResolver: () async =>
'${testRoot.path}/settings.sqlite3',
fallbackDirectoryPathResolver: () async => testRoot.path,
),
);
await _waitFor(() => !controller.initializing);
});
addTearDown(controller.dispose);
addTearDown(() async {
if (await testRoot.exists()) {
await testRoot.delete(recursive: true);
}
});
final staleGateway = controller.settings.aiGateway.copyWith(
name: 'default',

View File

@ -50,10 +50,7 @@ void main() {
SingleAgentProvider.codex,
SingleAgentProvider.claude,
],
gatewayOnlySkillScanRoots: <String>[
codexRoot.path,
claudeRoot.path,
],
gatewayOnlySkillScanRoots: <String>[codexRoot.path, claudeRoot.path],
);
addTearDown(controller.dispose);
await _waitFor(() => !controller.initializing);
@ -165,9 +162,10 @@ void main() {
);
await controller.switchSession('draft:thread-2');
expect(
controller.assistantImportedSkillsForSession(
controller.currentSessionKey,
).single.label,
controller
.assistantImportedSkillsForSession(controller.currentSessionKey)
.single
.label,
'Review',
);
await controller.selectAssistantModelForSession(