refactor(storage): unify persistent storage with robust error handling and simplified secret management
- Consolidate settings, tasks, and audit storage into SettingsStore and SecretStore - Implement PersistentWriteFailure for detailed error reporting across storage scopes - Migrate secret retrieval to rely primarily on reference-based lookups - Add ThemeMode persistence and AccountSyncState serialization - Modernize SecureConfigStore with clear path resolution and support for UI state - Streamline Rust build process by migrating from custom scripts to Makefile - Remove redundant build_rust_ffi.sh and update integration scripts
This commit is contained in:
parent
ec1fb1b5e7
commit
a04b22ec4a
6
Makefile
6
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
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<void> 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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -45,7 +45,7 @@ Future<String> 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<String> 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();
|
||||
}
|
||||
|
||||
@ -116,52 +116,24 @@ Future<String> 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<String> 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<String> 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<void> saveVaultTokenSettingsInternal(
|
||||
@ -331,16 +294,8 @@ Future<String> 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<void> saveAiGatewayApiKeySettingsInternal(
|
||||
@ -373,16 +328,8 @@ Future<String> 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<void> clearAiGatewayApiKeySettingsInternal(
|
||||
@ -440,15 +387,7 @@ Future<String> 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<String> readVaultSecretByRefSettingsInternal(
|
||||
|
||||
@ -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<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'mode': mode.name,
|
||||
|
||||
@ -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<String, dynamic>() ?? 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,
|
||||
|
||||
@ -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<String, String> get secureRefs => Map<String, String>.unmodifiable(_memorySecure);
|
||||
|
||||
PersistentWriteFailure? get secretsWriteFailure => _secretsWriteFailure;
|
||||
|
||||
Future<void> initialize() async {
|
||||
@ -254,6 +257,27 @@ class SecretStore {
|
||||
Future<void> clearAccountSessionSummary() =>
|
||||
_deleteSecure(_accountSessionSummaryKey);
|
||||
|
||||
Future<AccountSyncState?> loadAccountSyncState() async {
|
||||
final raw = await _readSecure(_accountSyncStateKey);
|
||||
if ((raw ?? '').trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return AccountSyncState.fromJson(
|
||||
(jsonDecode(raw!) as Map).cast<String, dynamic>(),
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveAccountSyncState(AccountSyncState value) =>
|
||||
_writeSecure(_accountSyncStateKey, jsonEncode(value.toJson()));
|
||||
|
||||
Future<void> clearAccountSyncState() => _deleteSecure(_accountSyncStateKey);
|
||||
|
||||
Future<Map<String, String>> loadAccountManagedSecrets() => loadSecureRefs();
|
||||
|
||||
Future<String?> loadAccountManagedSecret({required String target}) =>
|
||||
_readSecure(_accountManagedSecretKey(target));
|
||||
|
||||
|
||||
@ -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<String?> Function()? secretRootPathResolver,
|
||||
Future<String?> Function()? appDataRootPathResolver,
|
||||
Future<String?> 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<void> saveSettingsSnapshot(SettingsSnapshot snapshot) => _settingsStore.saveSnapshot(snapshot);
|
||||
Future<SettingsSnapshotReloadResult> reloadSettingsSnapshotResult() => _settingsStore.reloadSnapshotResult();
|
||||
|
||||
Future<String?> loadSecretValueByRef(String refName) => _secretStore.loadSecretValueByRef(refName);
|
||||
Future<void> saveSecretValueByRef(String refName, String value) => _secretStore.saveSecretValueByRef(refName, value);
|
||||
Future<void> clearSecretValueByRef(String refName) => _secretStore.clearSecretValueByRef(refName);
|
||||
|
||||
Future<Map<String, String>> loadAccountManagedSecrets() => _secretStore.loadAccountManagedSecrets();
|
||||
Future<String?> loadAccountManagedSecret({required String target}) => _secretStore.loadAccountManagedSecret(target: target);
|
||||
Future<void> saveAccountManagedSecret({required String target, required String value}) => _secretStore.saveAccountManagedSecret(target: target, value: value);
|
||||
Future<void> clearAccountManagedSecret({required String target}) => _secretStore.clearAccountManagedSecret(target: target);
|
||||
Future<void> clearAccountManagedSecrets() => _secretStore.clearAccountManagedSecrets();
|
||||
|
||||
Future<AccountSyncState?> loadAccountSyncState() => _secretStore.loadAccountSyncState();
|
||||
Future<void> saveAccountSyncState(AccountSyncState value) => _secretStore.saveAccountSyncState(value);
|
||||
Future<void> clearAccountSyncState() => _secretStore.clearAccountSyncState();
|
||||
|
||||
Future<List<SecretAuditEntry>> loadAuditTrail() => _settingsStore.loadAuditTrail();
|
||||
Future<void> appendAudit(SecretAuditEntry entry) => _settingsStore.appendAudit(entry);
|
||||
|
||||
Future<LocalDeviceIdentity?> loadDeviceIdentity() => _secretStore.loadDeviceIdentity();
|
||||
Future<void> saveDeviceIdentity(LocalDeviceIdentity identity) => _secretStore.saveDeviceIdentity(identity);
|
||||
Future<String?> loadDeviceToken({required String deviceId, required String role}) => _secretStore.loadDeviceToken(deviceId: deviceId, role: role);
|
||||
Future<void> saveDeviceToken({required String deviceId, required String role, required String token}) => _secretStore.saveDeviceToken(deviceId: deviceId, role: role, token: token);
|
||||
Future<void> clearDeviceToken({required String deviceId, required String role}) => _secretStore.clearDeviceToken(deviceId: deviceId, role: role);
|
||||
|
||||
Future<Map<String, String>> loadSecureRefs() => _secretStore.loadSecureRefs();
|
||||
|
||||
Future<String?> loadAccountSessionToken() => _secretStore.loadAccountSessionToken();
|
||||
Future<void> saveAccountSessionToken(String value) => _secretStore.saveAccountSessionToken(value);
|
||||
Future<void> clearAccountSessionToken() => _secretStore.clearAccountSessionToken();
|
||||
|
||||
Future<int?> loadAccountSessionExpiresAtMs() => _secretStore.loadAccountSessionExpiresAtMs();
|
||||
Future<int?> loadAccountSessionExpiresAtMs() => _secretStore.loadAccountSessionExpiresAtMs().then((v) => v == 0 ? null : v);
|
||||
Future<void> saveAccountSessionExpiresAtMs(int value) => _secretStore.saveAccountSessionExpiresAtMs(value);
|
||||
Future<void> clearAccountSessionExpiresAtMs() => _secretStore.clearAccountSessionExpiresAtMs();
|
||||
|
||||
@ -56,21 +91,78 @@ class SecureConfigStore {
|
||||
Future<void> saveAccountSessionSummary(AccountSessionSummary value) => _secretStore.saveAccountSessionSummary(value);
|
||||
Future<void> clearAccountSessionSummary() => _secretStore.clearAccountSessionSummary();
|
||||
|
||||
Future<AccountSyncState?> loadAccountSyncState() => _secretStore.loadAccountSyncState();
|
||||
Future<void> saveAccountSyncState(AccountSyncState value) => _secretStore.saveAccountSyncState(value);
|
||||
Future<void> clearAccountSyncState() => _secretStore.clearAccountSyncState();
|
||||
Future<List<TaskThread>> loadTaskThreads() => _settingsStore.loadTaskThreads();
|
||||
Future<void> saveTaskThreads(List<TaskThread> threads) => _settingsStore.saveTaskThreads(threads);
|
||||
List<SkippedTaskThreadRecord> get lastSkippedInvalidTaskThreadRecords => _settingsStore.lastSkippedInvalidTaskThreadRecords;
|
||||
|
||||
Future<Map<String, TaskThread>> loadTaskThreads() => _settingsStore.loadTaskThreads();
|
||||
Future<void> saveTaskThreads(Map<String, TaskThread> threads) => _settingsStore.saveTaskThreads(threads);
|
||||
Future<void> clearAssistantLocalState() => _settingsStore.clearAssistantLocalState();
|
||||
|
||||
Future<List<SecretAuditEntry>> loadAuditTrail() => _secretStore.loadAuditTrail();
|
||||
Future<void> appendAudit(SecretAuditEntry entry) => _secretStore.appendAudit(entry);
|
||||
Future<Map<String, dynamic>?> 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<String, dynamic> ? decoded : null;
|
||||
}
|
||||
|
||||
Future<void> saveSupportJson(
|
||||
String relativePath,
|
||||
Map<String, dynamic> payload,
|
||||
) async {
|
||||
final file = await supportFile(relativePath);
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
await atomicWriteString(file, jsonEncode(payload), ownerOnly: true);
|
||||
}
|
||||
|
||||
Future<File?> 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<AppUiState> 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<void> saveAppUiState(AppUiState value) =>
|
||||
saveSupportJson('ui/state.json', value.toJson());
|
||||
|
||||
Future<void> 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<File?> resolvedSettingsFile() => _layoutResolver.resolve().then((l) => File('${l.configDirectory.path}/settings.yaml'));
|
||||
Future<Directory?> resolvedSettingsWatchDirectory() => _layoutResolver.resolve().then((l) => l.configDirectory);
|
||||
|
||||
Map<String, String> get secureRefs => _secretStore.secureRefs;
|
||||
String? get settingsWriteFailure => _settingsStore.auditWriteFailure;
|
||||
PersistentWriteFailure? get settingsWriteFailure => _settingsStore.settingsWriteFailure;
|
||||
|
||||
void dispose() {
|
||||
_settingsStore.dispose();
|
||||
|
||||
@ -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<SkippedTaskThreadRecord> _lastSkippedInvalidTaskThreadRecords = [];
|
||||
List<SkippedTaskThreadRecord> get lastSkippedInvalidTaskThreadRecords => List.unmodifiable(_lastSkippedInvalidTaskThreadRecords);
|
||||
|
||||
Future<void> 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<Map<String, TaskThread>> loadTaskThreads() async {
|
||||
Future<List<TaskThread>> 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<String, dynamic>) {
|
||||
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<void> saveTaskThreads(List<TaskThread> 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<void> 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<List<SecretAuditEntry>> 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<void> saveTaskThreads(Map<String, TaskThread> threads) async {
|
||||
Future<void> 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() {}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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!"
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user