diff --git a/Makefile b/Makefile index 9bb7170c..8389b1ad 100644 --- a/Makefile +++ b/Makefile @@ -128,12 +128,12 @@ check-export-compliance: ## Verify source and built Apple plist export-complianc rust-build: rust-build-release ## Build Rust FFI library (release mode) -rust-build-release: ## Build Rust FFI library for macOS (universal) - bash scripts/build_rust_ffi.sh release +rust-build-release: ## Build Rust FFI library for macOS (release) + cd rust && cargo build --release @echo "Rust FFI library built successfully" rust-build-debug: ## Build Rust FFI library in debug mode - bash scripts/build_rust_ffi.sh debug + cd rust && cargo build rust-test: ## Run Rust tests cd rust && cargo test diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 5dca6fa4..61638281 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -11,6 +11,7 @@ import 'ui_feature_manifest.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; +import '../runtime/file_store_support.dart'; import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; @@ -18,6 +19,8 @@ import '../runtime/gateway_runtime.dart'; import '../runtime/account_runtime_client.dart'; import '../runtime/runtime_controllers.dart'; import '../runtime/runtime_models.dart'; +import '../runtime/secret_store.dart'; +import '../runtime/settings_store.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; diff --git a/lib/app/app_controller_desktop_settings.dart b/lib/app/app_controller_desktop_settings.dart index d62291e8..4e7ef582 100644 --- a/lib/app/app_controller_desktop_settings.dart +++ b/lib/app/app_controller_desktop_settings.dart @@ -18,6 +18,7 @@ import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; import '../runtime/runtime_controllers.dart'; import '../runtime/runtime_models.dart'; +import '../runtime/secret_store.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; diff --git a/lib/app/app_controller_desktop_settings_runtime.dart b/lib/app/app_controller_desktop_settings_runtime.dart index 562eace7..9bc2da6e 100644 --- a/lib/app/app_controller_desktop_settings_runtime.dart +++ b/lib/app/app_controller_desktop_settings_runtime.dart @@ -18,6 +18,8 @@ import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; import '../runtime/runtime_controllers.dart'; import '../runtime/runtime_models.dart'; +import '../runtime/secret_store.dart'; +import '../runtime/settings_store.dart'; import '../runtime/secure_config_store.dart'; import '../runtime/embedded_agent_launch_policy.dart'; import '../runtime/runtime_coordinator.dart'; @@ -755,6 +757,8 @@ extension AppControllerDesktopSettingsRuntime on AppController { await settingsControllerInternal.saveSnapshot(sanitized); settingsDraftInternal = sanitized; settingsDraftInitializedInternal = true; + } + Future applyPersistedSettingsSideEffectsInternal({ required SettingsSnapshot previous, required SettingsSnapshot current, @@ -780,12 +784,9 @@ extension AppControllerDesktopSettingsRuntime on AppController { if (refreshAfterSave || bridgeChanged) { // Re-trigger Bridge capability discovery if the mode or endpoint changed. - unawaited(refreshAcpCapabilitiesInternal(quiet: true)); + unawaited(refreshAcpCapabilitiesInternal()); } - notifyListeners(); - } - if (disposedInternal) { return; } diff --git a/lib/runtime/gateway_runtime_core.dart b/lib/runtime/gateway_runtime_core.dart index e65db39b..9634ec5d 100644 --- a/lib/runtime/gateway_runtime_core.dart +++ b/lib/runtime/gateway_runtime_core.dart @@ -13,6 +13,7 @@ import 'device_identity_store.dart'; import 'gateway_runtime_session_client.dart'; import 'platform_environment.dart'; import 'runtime_models.dart'; +import 'secret_store.dart'; import 'secure_config_store.dart'; import 'gateway_runtime_protocol.dart'; import 'gateway_runtime_events.dart'; @@ -135,17 +136,9 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal { ? SecretStore.gatewayPasswordRefKey(resolvedProfileIndex) : profile.passwordRef.trim(); final storedToken = - (await storeInternal.loadSecretValueByRef(tokenRef))?.trim() ?? - ((await storeInternal.loadGatewayToken( - profileIndex: profileIndex, - ))?.trim() ?? - ''); + (await storeInternal.loadSecretValueByRef(tokenRef))?.trim() ?? ''; final storedPassword = - (await storeInternal.loadSecretValueByRef(passwordRef))?.trim() ?? - ((await storeInternal.loadGatewayPassword( - profileIndex: profileIndex, - ))?.trim() ?? - ''); + (await storeInternal.loadSecretValueByRef(passwordRef))?.trim() ?? ''; final explicitToken = authTokenOverride.trim(); final explicitPassword = authPasswordOverride.trim(); final passwordSource = explicitPassword.isNotEmpty diff --git a/lib/runtime/runtime_controllers_settings.dart b/lib/runtime/runtime_controllers_settings.dart index 34a7f93e..11a83e76 100644 --- a/lib/runtime/runtime_controllers_settings.dart +++ b/lib/runtime/runtime_controllers_settings.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'account_runtime_client.dart'; import 'gateway_runtime.dart'; import 'runtime_models.dart'; +import 'secret_store.dart'; import 'secure_config_store.dart'; import 'runtime_controllers_gateway.dart'; import 'runtime_controllers_entities.dart'; diff --git a/lib/runtime/runtime_controllers_settings_connectivity_impl.dart b/lib/runtime/runtime_controllers_settings_connectivity_impl.dart index a31c75da..a832594a 100644 --- a/lib/runtime/runtime_controllers_settings_connectivity_impl.dart +++ b/lib/runtime/runtime_controllers_settings_connectivity_impl.dart @@ -45,7 +45,7 @@ Future testOllamaConnectionDraftSettingsInternal( } final cloudApiKey = apiKeyOverride.trim().isNotEmpty ? apiKeyOverride.trim() - : (await controller.storeInternal.loadOllamaCloudApiKey())?.trim() ?? ''; + : (await controller.storeInternal.loadSecretValueByRef('ollama_cloud_api_key'))?.trim() ?? ''; try { final uri = Uri.parse( cloud ? base : '$base${base.endsWith('/') ? '' : '/'}api/tags', @@ -103,7 +103,7 @@ Future testVaultConnectionDraftSettingsInternal( }; final token = tokenOverride.trim().isNotEmpty ? tokenOverride.trim() - : (await controller.storeInternal.loadVaultToken())?.trim() ?? ''; + : (await controller.storeInternal.loadSecretValueByRef('vault_token'))?.trim() ?? ''; if (token.trim().isNotEmpty) { headers['X-Vault-Token'] = token.trim(); } diff --git a/lib/runtime/runtime_controllers_settings_secrets_impl.dart b/lib/runtime/runtime_controllers_settings_secrets_impl.dart index fbc28ed1..9506ae3f 100644 --- a/lib/runtime/runtime_controllers_settings_secrets_impl.dart +++ b/lib/runtime/runtime_controllers_settings_secrets_impl.dart @@ -116,52 +116,24 @@ Future loadGatewayTokenSettingsInternal( SettingsController controller, { int? profileIndex, }) async { - if (profileIndex == null) { - return (await controller.storeInternal.loadGatewayToken())?.trim() ?? ''; - } final refName = gatewayTokenRefForProfileSettingsInternal( controller, - profileIndex, + profileIndex ?? kGatewayRemoteProfileIndex, ); - final byRef = - (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? + return (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? ''; - if (byRef.isNotEmpty) { - return byRef; - } - if (refName == SecretStore.gatewayTokenRefKey(profileIndex)) { - return (await controller.storeInternal.loadGatewayToken( - profileIndex: profileIndex, - ))?.trim() ?? - ''; - } - return ''; } Future loadGatewayPasswordSettingsInternal( SettingsController controller, { int? profileIndex, }) async { - if (profileIndex == null) { - return (await controller.storeInternal.loadGatewayPassword())?.trim() ?? ''; - } final refName = gatewayPasswordRefForProfileSettingsInternal( controller, - profileIndex, + profileIndex ?? kGatewayRemoteProfileIndex, ); - final byRef = - (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? + return (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? ''; - if (byRef.isNotEmpty) { - return byRef; - } - if (refName == SecretStore.gatewayPasswordRefKey(profileIndex)) { - return (await controller.storeInternal.loadGatewayPassword( - profileIndex: profileIndex, - ))?.trim() ?? - ''; - } - return ''; } bool hasStoredGatewayTokenForProfileSettingsInternal( @@ -288,17 +260,8 @@ Future loadOllamaCloudApiKeySettingsInternal( SettingsController controller, ) async { final refName = ollamaCloudApiKeyRefSettingsInternal(controller); - final byRef = - (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? + return (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? ''; - if (byRef.isNotEmpty) { - return byRef; - } - if (refName == 'ollama_cloud_api_key') { - return (await controller.storeInternal.loadOllamaCloudApiKey())?.trim() ?? - ''; - } - return ''; } Future saveVaultTokenSettingsInternal( @@ -331,16 +294,8 @@ Future loadVaultTokenSettingsInternal( SettingsController controller, ) async { final refName = vaultTokenRefSettingsInternal(controller); - final byRef = - (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? + return (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? ''; - if (byRef.isNotEmpty) { - return byRef; - } - if (refName == 'vault_token') { - return (await controller.storeInternal.loadVaultToken())?.trim() ?? ''; - } - return ''; } Future saveAiGatewayApiKeySettingsInternal( @@ -373,16 +328,8 @@ Future loadAiGatewayApiKeySettingsInternal( SettingsController controller, ) async { final refName = aiGatewayApiKeyRefSettingsInternal(controller); - final byRef = - (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? + return (await controller.storeInternal.loadSecretValueByRef(refName))?.trim() ?? ''; - if (byRef.isNotEmpty) { - return byRef; - } - if (refName == 'ai_gateway_api_key') { - return (await controller.storeInternal.loadAiGatewayApiKey())?.trim() ?? ''; - } - return ''; } Future clearAiGatewayApiKeySettingsInternal( @@ -440,15 +387,7 @@ Future loadVaultTokenForSecretReadsSettingsInternal( if (override.isNotEmpty) { return override; } - final token = await loadVaultTokenSettingsInternal(controller); - if (token.isNotEmpty) { - return token; - } - final refName = vaultTokenRefSettingsInternal(controller); - if (refName == 'vault_token') { - return (await controller.storeInternal.loadVaultToken())?.trim() ?? ''; - } - return ''; + return loadVaultTokenSettingsInternal(controller); } Future readVaultSecretByRefSettingsInternal( diff --git a/lib/runtime/runtime_models_account.dart b/lib/runtime/runtime_models_account.dart index 71e1ed60..4b175dd0 100644 --- a/lib/runtime/runtime_models_account.dart +++ b/lib/runtime/runtime_models_account.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'runtime_models_configs.dart'; import 'runtime_models_profiles.dart'; @@ -557,6 +558,8 @@ class AcpBridgeServerModeConfig { String get sourceTag => mode.name; + String toJsonString() => jsonEncode(toJson()); + Map toJson() { return { 'mode': mode.name, diff --git a/lib/runtime/runtime_models_settings_snapshot.dart b/lib/runtime/runtime_models_settings_snapshot.dart index ca0ae8fa..ed88a392 100644 --- a/lib/runtime/runtime_models_settings_snapshot.dart +++ b/lib/runtime/runtime_models_settings_snapshot.dart @@ -1,6 +1,7 @@ // ignore_for_file: unused_import, unnecessary_import import 'dart:convert'; +import 'package:flutter/material.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import 'runtime_models_account.dart'; @@ -34,6 +35,7 @@ class SettingsSnapshot { required this.aiGateway, required this.webSessionPersistence, required this.multiAgent, + required this.themeMode, required this.experimentalCanvas, required this.experimentalBridge, required this.experimentalDebug, @@ -66,6 +68,7 @@ class SettingsSnapshot { final AiGatewayProfile aiGateway; final WebSessionPersistenceConfig webSessionPersistence; final MultiAgentConfig multiAgent; + final ThemeMode themeMode; final bool experimentalCanvas; final bool experimentalBridge; final bool experimentalDebug; @@ -99,6 +102,7 @@ class SettingsSnapshot { aiGateway: AiGatewayProfile.defaults(), webSessionPersistence: WebSessionPersistenceConfig.defaults(), multiAgent: MultiAgentConfig.defaults(), + themeMode: ThemeMode.system, experimentalCanvas: false, experimentalBridge: false, experimentalDebug: false, @@ -133,6 +137,7 @@ class SettingsSnapshot { AiGatewayProfile? aiGateway, WebSessionPersistenceConfig? webSessionPersistence, MultiAgentConfig? multiAgent, + ThemeMode? themeMode, bool? experimentalCanvas, bool? experimentalBridge, bool? experimentalDebug, @@ -175,6 +180,7 @@ class SettingsSnapshot { webSessionPersistence: webSessionPersistence ?? this.webSessionPersistence, multiAgent: multiAgent ?? this.multiAgent, + themeMode: themeMode ?? this.themeMode, experimentalCanvas: experimentalCanvas ?? this.experimentalCanvas, experimentalBridge: experimentalBridge ?? this.experimentalBridge, experimentalDebug: experimentalDebug ?? this.experimentalDebug, @@ -218,6 +224,7 @@ class SettingsSnapshot { 'aiGateway': aiGateway.toJson(), 'webSessionPersistence': webSessionPersistence.toJson(), 'multiAgent': multiAgent.toJson(), + 'themeMode': themeMode.name, 'experimentalCanvas': experimentalCanvas, 'experimentalBridge': experimentalBridge, 'experimentalDebug': experimentalDebug, @@ -305,6 +312,10 @@ class SettingsSnapshot { multiAgent: MultiAgentConfig.fromJson( (json['multiAgent'] as Map?)?.cast() ?? const {}, ), + themeMode: ThemeMode.values.firstWhere( + (m) => m.name == json['themeMode'], + orElse: () => ThemeMode.system, + ), experimentalCanvas: json['experimentalCanvas'] as bool? ?? false, experimentalBridge: json['experimentalBridge'] as bool? ?? false, experimentalDebug: json['experimentalDebug'] as bool? ?? false, diff --git a/lib/runtime/secret_store.dart b/lib/runtime/secret_store.dart index 17d731a7..78fc077b 100644 --- a/lib/runtime/secret_store.dart +++ b/lib/runtime/secret_store.dart @@ -96,6 +96,7 @@ class SecretStore { 'xworkmate.account.session.identifier'; static const String _accountSessionSummaryKey = 'xworkmate.account.session.summary'; + static const String _accountSyncStateKey = 'xworkmate.account.sync_state'; static const String _customSecretRefRegistryKey = 'xworkmate.secret.ref_registry'; @@ -107,6 +108,8 @@ class SecretStore { bool _initialized = false; PersistentWriteFailure? _secretsWriteFailure; + Map get secureRefs => Map.unmodifiable(_memorySecure); + PersistentWriteFailure? get secretsWriteFailure => _secretsWriteFailure; Future initialize() async { @@ -254,6 +257,27 @@ class SecretStore { Future clearAccountSessionSummary() => _deleteSecure(_accountSessionSummaryKey); + Future loadAccountSyncState() async { + final raw = await _readSecure(_accountSyncStateKey); + if ((raw ?? '').trim().isEmpty) { + return null; + } + try { + return AccountSyncState.fromJson( + (jsonDecode(raw!) as Map).cast(), + ); + } catch (_) { + return null; + } + } + + Future saveAccountSyncState(AccountSyncState value) => + _writeSecure(_accountSyncStateKey, jsonEncode(value.toJson())); + + Future clearAccountSyncState() => _deleteSecure(_accountSyncStateKey); + + Future> loadAccountManagedSecrets() => loadSecureRefs(); + Future loadAccountManagedSecret({required String target}) => _readSecure(_accountManagedSecretKey(target)); diff --git a/lib/runtime/secure_config_store.dart b/lib/runtime/secure_config_store.dart index 9646851c..76e07416 100644 --- a/lib/runtime/secure_config_store.dart +++ b/lib/runtime/secure_config_store.dart @@ -1,9 +1,6 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:path_provider/package_info_plus.dart'; - import 'file_store_support.dart'; import 'runtime_models.dart'; import 'secret_store.dart'; @@ -11,12 +8,30 @@ import 'settings_store.dart'; class SecureConfigStore { SecureConfigStore({ + Future Function()? secretRootPathResolver, + Future Function()? appDataRootPathResolver, + Future Function()? supportRootPathResolver, StoreLayoutResolver? layoutResolver, SettingsStore? settingsStore, SecretStore? secretStore, - }) : _layoutResolver = layoutResolver ?? StoreLayoutResolver(), - _settingsStore = settingsStore ?? SettingsStore(layoutResolver ?? StoreLayoutResolver()), - _secretStore = secretStore ?? SecretStore(layoutResolver ?? StoreLayoutResolver()); + bool enableSecureStorage = true, + }) : _layoutResolver = layoutResolver ?? StoreLayoutResolver( + secretRootPathResolver: secretRootPathResolver, + appDataRootPathResolver: appDataRootPathResolver, + supportRootPathResolver: supportRootPathResolver, + ), + _settingsStore = settingsStore ?? SettingsStore(layoutResolver ?? StoreLayoutResolver( + appDataRootPathResolver: appDataRootPathResolver, + supportRootPathResolver: supportRootPathResolver, + )), + _secretStore = secretStore ?? SecretStore( + layoutResolver: layoutResolver ?? StoreLayoutResolver( + secretRootPathResolver: secretRootPathResolver, + appDataRootPathResolver: appDataRootPathResolver, + supportRootPathResolver: supportRootPathResolver, + ), + enableSecureStorage: enableSecureStorage, + ); final StoreLayoutResolver _layoutResolver; final SettingsStore _settingsStore; @@ -31,16 +46,36 @@ class SecureConfigStore { Future saveSettingsSnapshot(SettingsSnapshot snapshot) => _settingsStore.saveSnapshot(snapshot); Future reloadSettingsSnapshotResult() => _settingsStore.reloadSnapshotResult(); + Future loadSecretValueByRef(String refName) => _secretStore.loadSecretValueByRef(refName); + Future saveSecretValueByRef(String refName, String value) => _secretStore.saveSecretValueByRef(refName, value); + Future clearSecretValueByRef(String refName) => _secretStore.clearSecretValueByRef(refName); + Future> loadAccountManagedSecrets() => _secretStore.loadAccountManagedSecrets(); + Future loadAccountManagedSecret({required String target}) => _secretStore.loadAccountManagedSecret(target: target); Future saveAccountManagedSecret({required String target, required String value}) => _secretStore.saveAccountManagedSecret(target: target, value: value); Future clearAccountManagedSecret({required String target}) => _secretStore.clearAccountManagedSecret(target: target); Future clearAccountManagedSecrets() => _secretStore.clearAccountManagedSecrets(); + Future loadAccountSyncState() => _secretStore.loadAccountSyncState(); + Future saveAccountSyncState(AccountSyncState value) => _secretStore.saveAccountSyncState(value); + Future clearAccountSyncState() => _secretStore.clearAccountSyncState(); + + Future> loadAuditTrail() => _settingsStore.loadAuditTrail(); + Future appendAudit(SecretAuditEntry entry) => _settingsStore.appendAudit(entry); + + Future loadDeviceIdentity() => _secretStore.loadDeviceIdentity(); + Future saveDeviceIdentity(LocalDeviceIdentity identity) => _secretStore.saveDeviceIdentity(identity); + Future loadDeviceToken({required String deviceId, required String role}) => _secretStore.loadDeviceToken(deviceId: deviceId, role: role); + Future saveDeviceToken({required String deviceId, required String role, required String token}) => _secretStore.saveDeviceToken(deviceId: deviceId, role: role, token: token); + Future clearDeviceToken({required String deviceId, required String role}) => _secretStore.clearDeviceToken(deviceId: deviceId, role: role); + + Future> loadSecureRefs() => _secretStore.loadSecureRefs(); + Future loadAccountSessionToken() => _secretStore.loadAccountSessionToken(); Future saveAccountSessionToken(String value) => _secretStore.saveAccountSessionToken(value); Future clearAccountSessionToken() => _secretStore.clearAccountSessionToken(); - Future loadAccountSessionExpiresAtMs() => _secretStore.loadAccountSessionExpiresAtMs(); + Future loadAccountSessionExpiresAtMs() => _secretStore.loadAccountSessionExpiresAtMs().then((v) => v == 0 ? null : v); Future saveAccountSessionExpiresAtMs(int value) => _secretStore.saveAccountSessionExpiresAtMs(value); Future clearAccountSessionExpiresAtMs() => _secretStore.clearAccountSessionExpiresAtMs(); @@ -56,21 +91,78 @@ class SecureConfigStore { Future saveAccountSessionSummary(AccountSessionSummary value) => _secretStore.saveAccountSessionSummary(value); Future clearAccountSessionSummary() => _secretStore.clearAccountSessionSummary(); - Future loadAccountSyncState() => _secretStore.loadAccountSyncState(); - Future saveAccountSyncState(AccountSyncState value) => _secretStore.saveAccountSyncState(value); - Future clearAccountSyncState() => _secretStore.clearAccountSyncState(); + Future> loadTaskThreads() => _settingsStore.loadTaskThreads(); + Future saveTaskThreads(List threads) => _settingsStore.saveTaskThreads(threads); + List get lastSkippedInvalidTaskThreadRecords => _settingsStore.lastSkippedInvalidTaskThreadRecords; - Future> loadTaskThreads() => _settingsStore.loadTaskThreads(); - Future saveTaskThreads(Map threads) => _settingsStore.saveTaskThreads(threads); + Future clearAssistantLocalState() => _settingsStore.clearAssistantLocalState(); - Future> loadAuditTrail() => _secretStore.loadAuditTrail(); - Future appendAudit(SecretAuditEntry entry) => _secretStore.appendAudit(entry); + Future?> loadSupportJson(String relativePath) async { + final file = await supportFile(relativePath); + if (file == null || !await file.exists()) { + return null; + } + final raw = await file.readAsString(); + final decoded = jsonDecode(raw); + return decoded is Map ? decoded : null; + } + + Future saveSupportJson( + String relativePath, + Map payload, + ) async { + final file = await supportFile(relativePath); + if (file == null) { + return; + } + await atomicWriteString(file, jsonEncode(payload), ownerOnly: true); + } + + Future supportFile(String relativePath) async { + final normalized = relativePath.trim(); + if (normalized.isEmpty) { + return null; + } + final layout = await _layoutResolver.resolve(); + return File('${layout.rootDirectory.path}/$normalized'); + } + + Future loadAppUiState() async { + final payload = await loadSupportJson('ui/state.json'); + if (payload == null) { + return AppUiState.defaults(); + } + try { + return AppUiState.fromJson(payload); + } catch (_) { + return AppUiState.defaults(); + } + } + + Future saveAppUiState(AppUiState value) => + saveSupportJson('ui/state.json', value.toJson()); + + Future clearAppUiState() async { + final file = await supportFile('ui/state.json'); + if (file == null) { + return; + } + await deleteIfExists(file); + } + + PersistentWriteFailures get persistentWriteFailures => + PersistentWriteFailures( + settings: _settingsStore.auditWriteFailure, + tasks: _settingsStore.tasksWriteFailure, + secrets: _secretStore.secretsWriteFailure, + audit: _settingsStore.auditWriteFailure, + ); Future resolvedSettingsFile() => _layoutResolver.resolve().then((l) => File('${l.configDirectory.path}/settings.yaml')); Future resolvedSettingsWatchDirectory() => _layoutResolver.resolve().then((l) => l.configDirectory); Map get secureRefs => _secretStore.secureRefs; - String? get settingsWriteFailure => _settingsStore.auditWriteFailure; + PersistentWriteFailure? get settingsWriteFailure => _settingsStore.settingsWriteFailure; void dispose() { _settingsStore.dispose(); diff --git a/lib/runtime/settings_store.dart b/lib/runtime/settings_store.dart index 19f13d85..20382392 100644 --- a/lib/runtime/settings_store.dart +++ b/lib/runtime/settings_store.dart @@ -17,19 +17,45 @@ class SettingsSnapshotReloadResult { final SettingsSnapshot snapshot; } +enum SkippedTaskThreadReason { + removedAutoExecutionMode, + incompleteWorkspaceBinding, + invalidPersistedThreadData, +} + +class SkippedTaskThreadRecord { + const SkippedTaskThreadRecord({ + required this.threadId, + required this.reason, + }); + + final String threadId; + final SkippedTaskThreadReason reason; +} + class SettingsStore { SettingsStore(this._layoutResolver); final StoreLayoutResolver _layoutResolver; - String? _auditWriteFailure; - String? get auditWriteFailure => _auditWriteFailure; + + PersistentWriteFailure? _settingsWriteFailure; + PersistentWriteFailure? get settingsWriteFailure => _settingsWriteFailure; + + PersistentWriteFailure? _tasksWriteFailure; + PersistentWriteFailure? get tasksWriteFailure => _tasksWriteFailure; + + PersistentWriteFailure? _auditWriteFailure; + PersistentWriteFailure? get auditWriteFailure => _auditWriteFailure; + + final List _lastSkippedInvalidTaskThreadRecords = []; + List get lastSkippedInvalidTaskThreadRecords => List.unmodifiable(_lastSkippedInvalidTaskThreadRecords); Future initialize() async { // Basic connectivity check. try { await _layoutResolver.resolve(); } catch (e) { - _auditWriteFailure = 'Storage unavailable: $e'; + _settingsWriteFailure = _wrapFailure('initialize', PersistentStoreScope.settings, e); } } @@ -42,7 +68,7 @@ class SettingsStore { return SettingsSnapshot.fromJsonString(content); } } catch (e) { - _auditWriteFailure = 'Failed to load settings: $e'; + _settingsWriteFailure = _wrapFailure('loadSnapshot', PersistentStoreScope.settings, e); } return SettingsSnapshot.defaults(); } @@ -52,10 +78,9 @@ class SettingsStore { final layout = await _layoutResolver.resolve(); final file = File('${layout.configDirectory.path}/settings.yaml'); await file.writeAsString(snapshot.toJsonString(), flush: true); - _auditWriteFailure = null; + _settingsWriteFailure = null; } catch (e) { - _auditWriteFailure = 'Failed to save settings: $e'; - // In-memory fallback happens at Controller level via current snapshot retention. + _settingsWriteFailure = _wrapFailure('saveSnapshot', PersistentStoreScope.settings, e); } } @@ -64,32 +89,85 @@ class SettingsStore { return SettingsSnapshotReloadResult(applied: true, snapshot: next); } - Future> loadTaskThreads() async { + Future> loadTaskThreads() async { try { final layout = await _layoutResolver.resolve(); final file = File('${layout.tasksDirectory.path}/threads.json'); if (await file.exists()) { final content = await file.readAsString(); final decoded = jsonDecode(content); - if (decoded is Map) { - return decoded.map((key, value) => MapEntry(key, TaskThread.fromJson(value))); + if (decoded is List) { + return decoded.map((e) => TaskThread.fromJson(e)).toList(); + } + } + } catch (e) { + _tasksWriteFailure = _wrapFailure('loadTaskThreads', PersistentStoreScope.tasks, e); + } + return const []; + } + + Future saveTaskThreads(List threads) async { + try { + final layout = await _layoutResolver.resolve(); + final file = File('${layout.tasksDirectory.path}/threads.json'); + await file.writeAsString(jsonEncode(threads), flush: true); + _tasksWriteFailure = null; + } catch (e) { + _tasksWriteFailure = _wrapFailure('saveTaskThreads', PersistentStoreScope.tasks, e); + } + } + + Future clearAssistantLocalState() async { + try { + final layout = await _layoutResolver.resolve(); + await deleteIfExists(File('${layout.tasksDirectory.path}/threads.json')); + await deleteIfExists(File('${layout.configDirectory.path}/settings.yaml')); + } catch (_) { + // Ignore errors for secondary persistence. + } + } + + Future> loadAuditTrail() async { + try { + final layout = await _layoutResolver.resolve(); + final file = File('${layout.configDirectory.path}/audit.json'); + if (await file.exists()) { + final content = await file.readAsString(); + final decoded = jsonDecode(content); + if (decoded is List) { + return decoded.map((e) => SecretAuditEntry.fromJson(e)).toList(); } } } catch (_) { // Ignore errors for secondary persistence. } - return const {}; + return const []; } - Future saveTaskThreads(Map threads) async { + Future appendAudit(SecretAuditEntry entry) async { try { + final items = (await loadAuditTrail()).toList(growable: true); + items.insert(0, entry); + if (items.length > 40) { + items.removeRange(40, items.length); + } final layout = await _layoutResolver.resolve(); - final file = File('${layout.tasksDirectory.path}/threads.json'); - await file.writeAsString(jsonEncode(threads), flush: true); - } catch (_) { - // Ignore errors for secondary persistence. + final file = File('${layout.configDirectory.path}/audit.json'); + await file.writeAsString(jsonEncode(items), flush: true); + _auditWriteFailure = null; + } catch (e) { + _auditWriteFailure = _wrapFailure('appendAudit', PersistentStoreScope.audit, e); } } + PersistentWriteFailure _wrapFailure(String operation, PersistentStoreScope scope, Object error) { + return PersistentWriteFailure( + scope: scope, + operation: operation, + message: error.toString(), + timestampMs: DateTime.now().millisecondsSinceEpoch, + ); + } + void dispose() {} } diff --git a/macos/Frameworks/README.md b/macos/Frameworks/README.md index ec35af28..5ea0cc3e 100644 --- a/macos/Frameworks/README.md +++ b/macos/Frameworks/README.md @@ -11,7 +11,7 @@ The Rust FFI library for Codex CLI integration. Run the build script from the project root: ```bash -./scripts/build_rust_ffi.sh release +make rust-build-release ``` ### Integration diff --git a/scripts/build_rust_ffi.sh b/scripts/build_rust_ffi.sh deleted file mode 100755 index 8eb94855..00000000 --- a/scripts/build_rust_ffi.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -# Build Rust FFI library for macOS -# Usage: ./scripts/build_rust_ffi.sh [release|debug] - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -RUST_DIR="$PROJECT_ROOT/rust" - -BUILD_MODE="${1:-release}" -TARGET_DIR="$RUST_DIR/target" - -echo "Building codex-ffi ($BUILD_MODE)..." - -cd "$RUST_DIR" - -# Check if cargo is available -if ! command -v cargo &> /dev/null; then - echo "Error: cargo not found. Please install Rust: https://rustup.rs" - exit 1 -fi - -# Build for macOS (arm64 and x86_64) -if [[ "$BUILD_MODE" == "release" ]]; then - echo "Building release mode..." - cargo build --release --target aarch64-apple-darwin - cargo build --release --target x86_64-apple-darwin - - # Create universal binary - mkdir -p "$TARGET_DIR/universal" - lipo -create \ - "$TARGET_DIR/aarch64-apple-darwin/release/libcodex_ffi.a" \ - "$TARGET_DIR/x86_64-apple-darwin/release/libcodex_ffi.a" \ - -output "$TARGET_DIR/universal/libcodex_ffi.a" - - lipo -create \ - "$TARGET_DIR/aarch64-apple-darwin/release/libcodex_ffi.dylib" \ - "$TARGET_DIR/x86_64-apple-darwin/release/libcodex_ffi.dylib" \ - -output "$TARGET_DIR/universal/libcodex_ffi.dylib" - - echo "Universal binary created at $TARGET_DIR/universal/" -else - echo "Building debug mode..." - cargo build --target aarch64-apple-darwin - cargo build --target x86_64-apple-darwin -fi - -# Copy to macOS Frameworks directory -FRAMEWORKS_DIR="$PROJECT_ROOT/macos/Frameworks" -mkdir -p "$FRAMEWORKS_DIR" - -if [[ "$BUILD_MODE" == "release" ]]; then - cp "$TARGET_DIR/universal/libcodex_ffi.dylib" "$FRAMEWORKS_DIR/" -else - cp "$TARGET_DIR/aarch64-apple-darwin/debug/libcodex_ffi.dylib" "$FRAMEWORKS_DIR/" -fi - -echo "Library copied to $FRAMEWORKS_DIR/" -echo "Build complete!" diff --git a/scripts/copy_ffi_framework.sh b/scripts/copy_ffi_framework.sh index 769f747a..087124f2 100755 --- a/scripts/copy_ffi_framework.sh +++ b/scripts/copy_ffi_framework.sh @@ -28,7 +28,7 @@ elif [[ -f "$DEBUG_LIB" ]]; then echo "Copying debug FFI library..." cp "$DEBUG_LIB" "$FRAMEWORKS_DIR/" else - echo "Warning: FFI library not found. Run scripts/build_rust_ffi.sh first." + echo "Warning: FFI library not found. Run make rust-build-release first." echo "Expected one of:" echo " - $UNIVERSAL_LIB" echo " - $ARM_LIB" diff --git a/scripts/integrate_rust_flutter.sh b/scripts/integrate_rust_flutter.sh index b8e0aba3..bdbddccd 100755 --- a/scripts/integrate_rust_flutter.sh +++ b/scripts/integrate_rust_flutter.sh @@ -13,7 +13,8 @@ echo "Integrating Rust FFI with Flutter..." RUST_LIB="$PROJECT_ROOT/rust/target/universal/libcodex_ffi.dylib" if [[ ! -f "$RUST_LIB" ]]; then echo "Rust library not found, building..." - "$SCRIPT_DIR/build_rust_ffi.sh" release + # Attempt to build using Makefile target if available + (cd "$PROJECT_ROOT" && make rust-build-release) fi # Ensure Frameworks directory exists @@ -31,7 +32,7 @@ else cp "$ARM_LIB" "$FRAMEWORKS_DIR/" echo "Copied arm64 library to $FRAMEWORKS_DIR/" else - echo "Error: No Rust library found. Please run scripts/build_rust_ffi.sh first." + echo "Error: No Rust library found. Please run 'make rust-build-release' first." exit 1 fi fi