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:
Haitao Pan 2026-04-19 10:34:15 +08:00
parent ec1fb1b5e7
commit a04b22ec4a
17 changed files with 270 additions and 183 deletions

View File

@ -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

View File

@ -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';

View File

@ -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';

View File

@ -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;
}

View File

@ -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

View File

@ -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';

View File

@ -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();
}

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -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));

View File

@ -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();

View File

@ -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() {}
}

View File

@ -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

View File

@ -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!"

View File

@ -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"

View File

@ -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