const std = @import("std");const agent_steps = @import("agent_steps.zig");const debug_trace = @import("../shared/debug_trace.zig");const host = @import("../hosts/host.zig");const io_mod = @import("../shared/io.zig");const profile_paths = @import("../shared/profile_paths.zig");const tool_result_limits = @import("../tooling/tool_result_limits.zig");const types = @import("../shared/types.zig");const sandbox = @import("../permissions/sandbox.zig");const workspace_access = @import("../workspace/workspace_access.zig");const settings_store = @import("settings_store.zig");const update_target = @import("../upgrade/update_target.zig");pub const context_limits = @import("context_limits.zig"); const Allocator = std.mem.Allocator;const max_settings_bytes: usize = 64 * 1024;pub const default_permission_mode: types.PermissionMode = .auto; pub const Paths = struct { home_dir: ?[]u8 = null, user_settings: ?[]u8 = null, workspace_settings: []u8, home_fx_dir: ?[]u8 = null, sessions_dir: ?[]u8 = null, workspace_root: []u8, pub fn deinit(self: *Paths, alloc: Allocator) void { if (self.home_dir) |path| alloc.free(path); if (self.user_settings) |path| alloc.free(path); alloc.free(self.workspace_settings); if (self.home_fx_dir) |path| alloc.free(path); if (self.sessions_dir) |path| alloc.free(path); alloc.free(self.workspace_root); self.* = undefined; }}; pub const Settings = struct { model: ?[]u8 = null, permission_mode: ?types.PermissionMode = null, credential_source: ?types.CredentialSource = null, yolo_acknowledged: ?bool = null, max_agent_steps: ?usize = null, max_tool_result_bytes: ?usize = null, context_limits: context_limits.Overrides = .{}, first_call_tool_choice: ?types.ToolChoice = null, context: ?bool = null, fast_mode: ?bool = null, input_appearance: ?[]u8 = null, maxxing_mode: ?[]u8 = null, slash_menu_categories: ?bool = null, auto_upgrade: ?bool = null, update_channel: ?update_target.Channel = null, startup_scrollback: ?bool = null, prompt_history_enabled: ?bool = null, effort: ?types.ReasoningEffort = null, sandbox: ?[]u8 = null, statusline_sandbox: ?bool = null, statusline_context: ?bool = null, statusline_session: ?bool = null, notification_turn_end: ?bool = null, notification_attention_required: ?bool = null, notification_max: ?bool = null, permission_rules: types.PermissionRuleSet = .{}, has_permission_rules: bool = false, pub fn deinit(self: *Settings, alloc: Allocator) void { if (self.model) |value| alloc.free(value); if (self.input_appearance) |value| alloc.free(value); if (self.maxxing_mode) |value| alloc.free(value); if (self.sandbox) |value| alloc.free(value); self.permission_rules.deinit(alloc); self.* = .{}; }}; pub const StartupStatusSettings = struct { model: ?[]u8 = null, permission_mode: ?types.PermissionMode = null, sandbox: ?[]u8 = null, max_agent_steps: ?usize = null, pub fn deinit(self: *StartupStatusSettings, alloc: Allocator) void { if (self.model) |value| alloc.free(value); if (self.sandbox) |value| alloc.free(value); self.* = .{}; }}; pub const ConfigLayer = enum { user, project,}; pub const ConfigSource = enum { compiled_default, user_global, project, user_workspace, process_override,}; pub const ModelSource = ConfigSource; pub const ConfigSources = struct { model: ConfigSource = .compiled_default, permission_mode: ConfigSource = .compiled_default, effort: ConfigSource = .compiled_default, fast_mode: ConfigSource = .compiled_default, input_appearance: ConfigSource = .compiled_default, maxxing_mode: ConfigSource = .compiled_default, slash_menu_categories: ConfigSource = .compiled_default, startup_scrollback: ConfigSource = .compiled_default, prompt_history_enabled: ConfigSource = .compiled_default, statusline_sandbox: ConfigSource = .compiled_default, statusline_context: ConfigSource = .compiled_default, statusline_session: ConfigSource = .compiled_default, notification_turn_end: ConfigSource = .compiled_default, notification_attention_required: ConfigSource = .compiled_default, notification_max: ConfigSource = .compiled_default, sandbox: ConfigSource = .compiled_default,}; pub fn resolveContextLimits(settings: *const Settings, command_line: []const context_limits.Override) context_limits.Values { var values = context_limits.Values{}; values.apply(settings.context_limits); values.applyCommandLine(command_line); return values;} pub const PermissionSourceViews = struct { user: types.PermissionRuleSet = .{}, local: types.PermissionRuleSet = .{}, user_shadowed_by_local: bool = false, pub fn deinit(self: *PermissionSourceViews, alloc: Allocator) void { self.user.deinit(alloc); self.local.deinit(alloc); self.* = .{}; }}; pub const ConfigDiagnosticCause = enum { malformed_settings, settings_too_large, durable_path_unsafe, private_state_permissions_unsupported, invalid_model_id, ignored_project_user_only_setting, legacy_workspace_preferences, manual_backup_available, retired_skill_match_fuzzy, invalid_context_limits, invalid_additional_directories,}; pub const ConfigDiagnostic = struct { layer: ConfigLayer, cause: ConfigDiagnosticCause, setting_key: ?[]u8 = null, recovery_path: ?[]u8 = null, pub fn reportAtStartup(self: ConfigDiagnostic) bool { return self.cause != .legacy_workspace_preferences; } pub fn deinit(self: *ConfigDiagnostic, alloc: Allocator) void { if (self.setting_key) |key| alloc.free(key); if (self.recovery_path) |path| alloc.free(path); self.* = undefined; }}; pub fn writeDiagnosticMetadata(writer: *std.Io.Writer, diagnostic: ConfigDiagnostic) !void { if (diagnostic.setting_key) |key| try writer.print("; key={s}", .{key}); if (diagnostic.recovery_path) |path| try writer.print("; recovery={s}", .{path}); if (diagnostic.cause == .retired_skill_match_fuzzy) { try writer.writeAll("; remove skill_match_fuzzy; skills now load only through explicit invocation or the skill tool"); } if (diagnostic.cause == .invalid_context_limits) { try writer.writeAll("; context_limits keys must be documented limit names with a non-negative integer or \"off\" value"); } if (diagnostic.cause == .invalid_additional_directories) { try writer.print( "; additional_directories must be an array of at most {d} unique absolute directory paths for the current primary workspace", .{workspace_access.max_additional_directories}, ); }} pub const DetailedSettings = struct { settings: Settings, diagnostics: []ConfigDiagnostic = &.{}, model_source: ?ModelSource = null, sources: ConfigSources = .{}, permission_sources: PermissionSourceViews = .{}, prompt_history_store_allowed: bool = true, additional_directories: ?[][]u8 = null, additional_directory_sources: ?[][]u8 = null, pub fn deinit(self: *DetailedSettings, alloc: Allocator) void { self.settings.deinit(alloc); self.permission_sources.deinit(alloc); if (self.additional_directories) |paths| freeStringSlice(alloc, paths); if (self.additional_directory_sources) |paths| freeStringSlice(alloc, paths); if (self.diagnostics.len > 0) { for (self.diagnostics) |*diagnostic| diagnostic.deinit(alloc); alloc.free(self.diagnostics); } self.* = undefined; }}; pub fn discoverPaths(alloc: Allocator, workspace_root: []const u8) !Paths { return discoverPathsWithOptionalHome(alloc, io_mod.getenv("HOME"), workspace_root);} pub fn discoverPathsFromHome(alloc: Allocator, home_dir: []const u8, workspace_root: []const u8) !Paths { return discoverPathsWithOptionalHome(alloc, home_dir, workspace_root);} pub fn loadMergedSettings(alloc: Allocator, workspace_root: []const u8) !Settings { var paths = try discoverPaths(alloc, workspace_root); defer paths.deinit(alloc); return loadMergedSettingsFromPaths(alloc, paths);} pub fn loadMergedSettingsFromHome(alloc: Allocator, home_dir: []const u8, workspace_root: []const u8) !Settings { var paths = try discoverPathsFromHome(alloc, home_dir, workspace_root); defer paths.deinit(alloc); return loadMergedSettingsFromPaths(alloc, paths);} pub fn loadMergedSettingsDetailedFromHome( alloc: Allocator, home_dir: []const u8, workspace_root: []const u8,) !DetailedSettings { return loadMergedSettingsDetailedWithOptionalHome(alloc, home_dir, workspace_root);} pub fn loadMergedSettingsDetailed(alloc: Allocator, workspace_root: []const u8) !DetailedSettings { return loadMergedSettingsDetailedWithOptionalHome(alloc, io_mod.getenv("HOME"), workspace_root);} fn loadMergedSettingsDetailedWithOptionalHome( alloc: Allocator, home_dir: ?[]const u8, workspace_root: []const u8,) !DetailedSettings { var settings = Settings{}; errdefer settings.deinit(alloc); var diagnostics: std.ArrayList(ConfigDiagnostic) = .empty; errdefer { for (diagnostics.items) |*diagnostic| diagnostic.deinit(alloc); diagnostics.deinit(alloc); } var sources = ConfigSources{}; var permission_sources = PermissionSourceViews{}; errdefer permission_sources.deinit(alloc); var prompt_history_store_allowed = home_dir != null; var additional_directories: ?[][]u8 = null; errdefer if (additional_directories) |paths| freeStringSlice(alloc, paths); var additional_directory_sources: ?[][]u8 = null; errdefer if (additional_directory_sources) |paths| freeStringSlice(alloc, paths); const detailed_merge_state: DetailedSettingsMergeState = .{ .settings = &settings, .sources = &sources, .permission_sources = &permission_sources, .diagnostics = &diagnostics, .prompt_history_store_allowed = &prompt_history_store_allowed, }; var user_root: ?std.json.Parsed(std.json.Value) = null; defer if (user_root) |*parsed| parsed.deinit(); if (home_dir) |home| { var store: ?settings_store.Store = settings_store.Store.initFromHome(alloc, home, .read_only) catch |err| blk: { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = diagnosticCauseForUserStoreError(err), }); break :blk null; }; if (store) |*usable_store| { defer usable_store.deinit(alloc); var primary = usable_store.loadPrimary(alloc) catch |err| blk: { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = diagnosticCauseForUserStoreError(err), }); break :blk settings_store.PrimaryLoad.absent; }; defer primary.deinit(alloc); switch (primary) { .absent => {}, .invalid => { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = .malformed_settings }); if (usable_store.newestValidBackupPath(alloc) catch null) |recovery_path| { try diagnostics.append(alloc, .{ .layer = .user, .cause = .manual_backup_available, .recovery_path = recovery_path, }); } }, .oversized => { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = .settings_too_large }); if (usable_store.newestValidBackupPath(alloc) catch null) |recovery_path| { try diagnostics.append(alloc, .{ .layer = .user, .cause = .manual_backup_available, .recovery_path = recovery_path, }); } }, .valid => |bytes| { user_root = try std.json.parseFromSlice(std.json.Value, alloc, bytes, .{ .allocate = .alloc_always }); if (user_root.?.value != .object) { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = .malformed_settings }); user_root.?.deinit(); user_root = null; } }, } } } const project_path = try std.fs.path.join(alloc, &.{ workspace_root, ".fx.json" }); defer alloc.free(project_path); const project_text = readOptionalFile(alloc, project_path) catch |err| blk: { if (err == error.OutOfMemory) return err; try diagnostics.append(alloc, .{ .layer = .project, .cause = switch (err) { error.StreamTooLong => .settings_too_large, error.DurablePathUnsafe => .durable_path_unsafe, else => .malformed_settings, }, }); break :blk null; }; defer if (project_text) |text| alloc.free(text); if (project_text) |text| { if (std.json.parseFromSlice(std.json.Value, alloc, text, .{})) |parsed_value| { var parsed = parsed_value; defer parsed.deinit(); if (parsed.value != .object) { try diagnostics.append(alloc, .{ .layer = .project, .cause = .malformed_settings }); } else { try appendIgnoredProjectProfileSettingDiagnostics(alloc, &diagnostics, parsed.value); try mergeDetailedSettingsLayer( alloc, &detailed_merge_state, parsed.value, .project, false, .project, .project, .none, ); } } else |err| { if (err == error.OutOfMemory) return err; try diagnostics.append(alloc, .{ .layer = .project, .cause = .malformed_settings }); } } if (user_root) |parsed| { if (parsed.value.object.contains("additional_directories")) { try diagnostics.append(alloc, .{ .layer = .user, .cause = .invalid_additional_directories, .setting_key = try alloc.dupe(u8, "additional_directories"), }); } if (hasLegacyWorkspacePreferences(parsed.value)) { try diagnostics.append(alloc, .{ .layer = .user, .cause = .legacy_workspace_preferences, }); } try mergeDetailedSettingsLayer( alloc, &detailed_merge_state, parsed.value, .profile, false, .user, .user_global, .user, ); } if (user_root) |parsed| { const workspaces = parsed.value.object.get("workspaces"); if (workspaces) |workspaces_value| { if (workspaces_value != .object) { try diagnostics.append(alloc, .{ .layer = .user, .cause = .malformed_settings }); } else if (workspaces_value.object.get(workspace_root)) |workspace_value| { if (workspace_value != .object) { try diagnostics.append(alloc, .{ .layer = .user, .cause = .malformed_settings }); } else { if (workspace_value.object.get("additional_directories")) |value| { const parsed_directories = parseAdditionalDirectories( alloc, workspace_root, value, ) catch |err| blk: { if (err == error.OutOfMemory) return err; try diagnostics.append(alloc, .{ .layer = .user, .cause = .invalid_additional_directories, .setting_key = try alloc.dupe(u8, "additional_directories"), }); break :blk null; }; if (parsed_directories) |directories| { additional_directories = directories.canonical; additional_directory_sources = directories.sources; } } try mergeDetailedSettingsLayer( alloc, &detailed_merge_state, workspace_value, .profile, true, .user, .user_workspace, .local, ); } } } } if (io_mod.getenv("FX_MODEL")) |model_override| { if (std.mem.trim(u8, model_override, " \t\r\n").len > 0) { sources.model = .process_override; } } return .{ .settings = settings, .diagnostics = try diagnostics.toOwnedSlice(alloc), .model_source = sources.model, .sources = sources, .permission_sources = permission_sources, .prompt_history_store_allowed = prompt_history_store_allowed, .additional_directories = additional_directories, .additional_directory_sources = additional_directory_sources, };} const ParsedAdditionalDirectories = struct { canonical: [][]u8, sources: [][]u8,}; fn parseAdditionalDirectories( alloc: Allocator, workspace_root: []const u8, value: std.json.Value,) !ParsedAdditionalDirectories { if (value != .array) return error.InvalidAdditionalDirectories; var raw_paths: std.ArrayList([]const u8) = .empty; defer raw_paths.deinit(alloc); for (value.array.items) |item| { if (item != .string) return error.InvalidAdditionalDirectories; if (raw_paths.items.len >= workspace_access.max_additional_directories) { return error.InvalidAdditionalDirectories; } for (raw_paths.items) |path| { if (std.mem.eql(u8, path, item.string)) return error.InvalidAdditionalDirectories; } try raw_paths.append(alloc, item.string); } var access = workspace_access.WorkspaceAccess.init( alloc, workspace_root, raw_paths.items, &.{}, false, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.InvalidAdditionalDirectories, }; defer access.deinit(alloc); const canonical = try access.savedDirectoriesAlloc(alloc); errdefer freeStringSlice(alloc, canonical); return .{ .canonical = canonical,Lines 1–500 of 3671. Load more
const std = @import("std"); const agent_steps = @import("agent_steps.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const host = @import("../hosts/host.zig"); const io_mod = @import("../shared/io.zig"); const profile_paths = @import("../shared/profile_paths.zig"); const tool_result_limits = @import("../tooling/tool_result_limits.zig"); const types = @import("../shared/types.zig"); const sandbox = @import("../permissions/sandbox.zig"); const workspace_access = @import("../workspace/workspace_access.zig"); const settings_store = @import("settings_store.zig"); const update_target = @import("../upgrade/update_target.zig"); pub const context_limits = @import("context_limits.zig"); const Allocator = std.mem.Allocator; const max_settings_bytes: usize = 64 * 1024; pub const default_permission_mode: types.PermissionMode = .auto; pub const Paths = struct { home_dir: ?[]u8 = null, user_settings: ?[]u8 = null, workspace_settings: []u8, home_fx_dir: ?[]u8 = null, sessions_dir: ?[]u8 = null, workspace_root: []u8, pub fn deinit(self: *Paths, alloc: Allocator) void { if (self.home_dir) |path| alloc.free(path); if (self.user_settings) |path| alloc.free(path); alloc.free(self.workspace_settings); if (self.home_fx_dir) |path| alloc.free(path); if (self.sessions_dir) |path| alloc.free(path); alloc.free(self.workspace_root); self.* = undefined; } }; pub const Settings = struct { model: ?[]u8 = null, permission_mode: ?types.PermissionMode = null, credential_source: ?types.CredentialSource = null, yolo_acknowledged: ?bool = null, max_agent_steps: ?usize = null, max_tool_result_bytes: ?usize = null, context_limits: context_limits.Overrides = .{}, first_call_tool_choice: ?types.ToolChoice = null, context: ?bool = null, fast_mode: ?bool = null, input_appearance: ?[]u8 = null, maxxing_mode: ?[]u8 = null, slash_menu_categories: ?bool = null, auto_upgrade: ?bool = null, update_channel: ?update_target.Channel = null, startup_scrollback: ?bool = null, prompt_history_enabled: ?bool = null, effort: ?types.ReasoningEffort = null, sandbox: ?[]u8 = null, statusline_sandbox: ?bool = null, statusline_context: ?bool = null, statusline_session: ?bool = null, notification_turn_end: ?bool = null, notification_attention_required: ?bool = null, notification_max: ?bool = null, permission_rules: types.PermissionRuleSet = .{}, has_permission_rules: bool = false, pub fn deinit(self: *Settings, alloc: Allocator) void { if (self.model) |value| alloc.free(value); if (self.input_appearance) |value| alloc.free(value); if (self.maxxing_mode) |value| alloc.free(value); if (self.sandbox) |value| alloc.free(value); self.permission_rules.deinit(alloc); self.* = .{}; } }; pub const StartupStatusSettings = struct { model: ?[]u8 = null, permission_mode: ?types.PermissionMode = null, sandbox: ?[]u8 = null, max_agent_steps: ?usize = null, pub fn deinit(self: *StartupStatusSettings, alloc: Allocator) void { if (self.model) |value| alloc.free(value); if (self.sandbox) |value| alloc.free(value); self.* = .{}; } }; pub const ConfigLayer = enum { user, project, }; pub const ConfigSource = enum { compiled_default, user_global, project, user_workspace, process_override, }; pub const ModelSource = ConfigSource; pub const ConfigSources = struct { model: ConfigSource = .compiled_default, permission_mode: ConfigSource = .compiled_default, effort: ConfigSource = .compiled_default, fast_mode: ConfigSource = .compiled_default, input_appearance: ConfigSource = .compiled_default, maxxing_mode: ConfigSource = .compiled_default, slash_menu_categories: ConfigSource = .compiled_default, startup_scrollback: ConfigSource = .compiled_default, prompt_history_enabled: ConfigSource = .compiled_default, statusline_sandbox: ConfigSource = .compiled_default, statusline_context: ConfigSource = .compiled_default, statusline_session: ConfigSource = .compiled_default, notification_turn_end: ConfigSource = .compiled_default, notification_attention_required: ConfigSource = .compiled_default, notification_max: ConfigSource = .compiled_default, sandbox: ConfigSource = .compiled_default, }; pub fn resolveContextLimits(settings: *const Settings, command_line: []const context_limits.Override) context_limits.Values { var values = context_limits.Values{}; values.apply(settings.context_limits); values.applyCommandLine(command_line); return values; } pub const PermissionSourceViews = struct { user: types.PermissionRuleSet = .{}, local: types.PermissionRuleSet = .{}, user_shadowed_by_local: bool = false, pub fn deinit(self: *PermissionSourceViews, alloc: Allocator) void { self.user.deinit(alloc); self.local.deinit(alloc); self.* = .{}; } }; pub const ConfigDiagnosticCause = enum { malformed_settings, settings_too_large, durable_path_unsafe, private_state_permissions_unsupported, invalid_model_id, ignored_project_user_only_setting, legacy_workspace_preferences, manual_backup_available, retired_skill_match_fuzzy, invalid_context_limits, invalid_additional_directories, }; pub const ConfigDiagnostic = struct { layer: ConfigLayer, cause: ConfigDiagnosticCause, setting_key: ?[]u8 = null, recovery_path: ?[]u8 = null, pub fn reportAtStartup(self: ConfigDiagnostic) bool { return self.cause != .legacy_workspace_preferences; } pub fn deinit(self: *ConfigDiagnostic, alloc: Allocator) void { if (self.setting_key) |key| alloc.free(key); if (self.recovery_path) |path| alloc.free(path); self.* = undefined; } }; pub fn writeDiagnosticMetadata(writer: *std.Io.Writer, diagnostic: ConfigDiagnostic) !void { if (diagnostic.setting_key) |key| try writer.print("; key={s}", .{key}); if (diagnostic.recovery_path) |path| try writer.print("; recovery={s}", .{path}); if (diagnostic.cause == .retired_skill_match_fuzzy) { try writer.writeAll("; remove skill_match_fuzzy; skills now load only through explicit invocation or the skill tool"); } if (diagnostic.cause == .invalid_context_limits) { try writer.writeAll("; context_limits keys must be documented limit names with a non-negative integer or \"off\" value"); } if (diagnostic.cause == .invalid_additional_directories) { try writer.print( "; additional_directories must be an array of at most {d} unique absolute directory paths for the current primary workspace", .{workspace_access.max_additional_directories}, ); } } pub const DetailedSettings = struct { settings: Settings, diagnostics: []ConfigDiagnostic = &.{}, model_source: ?ModelSource = null, sources: ConfigSources = .{}, permission_sources: PermissionSourceViews = .{}, prompt_history_store_allowed: bool = true, additional_directories: ?[][]u8 = null, additional_directory_sources: ?[][]u8 = null, pub fn deinit(self: *DetailedSettings, alloc: Allocator) void { self.settings.deinit(alloc); self.permission_sources.deinit(alloc); if (self.additional_directories) |paths| freeStringSlice(alloc, paths); if (self.additional_directory_sources) |paths| freeStringSlice(alloc, paths); if (self.diagnostics.len > 0) { for (self.diagnostics) |*diagnostic| diagnostic.deinit(alloc); alloc.free(self.diagnostics); } self.* = undefined; } }; pub fn discoverPaths(alloc: Allocator, workspace_root: []const u8) !Paths { return discoverPathsWithOptionalHome(alloc, io_mod.getenv("HOME"), workspace_root); } pub fn discoverPathsFromHome(alloc: Allocator, home_dir: []const u8, workspace_root: []const u8) !Paths { return discoverPathsWithOptionalHome(alloc, home_dir, workspace_root); } pub fn loadMergedSettings(alloc: Allocator, workspace_root: []const u8) !Settings { var paths = try discoverPaths(alloc, workspace_root); defer paths.deinit(alloc); return loadMergedSettingsFromPaths(alloc, paths); } pub fn loadMergedSettingsFromHome(alloc: Allocator, home_dir: []const u8, workspace_root: []const u8) !Settings { var paths = try discoverPathsFromHome(alloc, home_dir, workspace_root); defer paths.deinit(alloc); return loadMergedSettingsFromPaths(alloc, paths); } pub fn loadMergedSettingsDetailedFromHome( alloc: Allocator, home_dir: []const u8, workspace_root: []const u8, ) !DetailedSettings { return loadMergedSettingsDetailedWithOptionalHome(alloc, home_dir, workspace_root); } pub fn loadMergedSettingsDetailed(alloc: Allocator, workspace_root: []const u8) !DetailedSettings { return loadMergedSettingsDetailedWithOptionalHome(alloc, io_mod.getenv("HOME"), workspace_root); } fn loadMergedSettingsDetailedWithOptionalHome( alloc: Allocator, home_dir: ?[]const u8, workspace_root: []const u8, ) !DetailedSettings { var settings = Settings{}; errdefer settings.deinit(alloc); var diagnostics: std.ArrayList(ConfigDiagnostic) = .empty; errdefer { for (diagnostics.items) |*diagnostic| diagnostic.deinit(alloc); diagnostics.deinit(alloc); } var sources = ConfigSources{}; var permission_sources = PermissionSourceViews{}; errdefer permission_sources.deinit(alloc); var prompt_history_store_allowed = home_dir != null; var additional_directories: ?[][]u8 = null; errdefer if (additional_directories) |paths| freeStringSlice(alloc, paths); var additional_directory_sources: ?[][]u8 = null; errdefer if (additional_directory_sources) |paths| freeStringSlice(alloc, paths); const detailed_merge_state: DetailedSettingsMergeState = .{ .settings = &settings, .sources = &sources, .permission_sources = &permission_sources, .diagnostics = &diagnostics, .prompt_history_store_allowed = &prompt_history_store_allowed, }; var user_root: ?std.json.Parsed(std.json.Value) = null; defer if (user_root) |*parsed| parsed.deinit(); if (home_dir) |home| { var store: ?settings_store.Store = settings_store.Store.initFromHome(alloc, home, .read_only) catch |err| blk: { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = diagnosticCauseForUserStoreError(err), }); break :blk null; }; if (store) |*usable_store| { defer usable_store.deinit(alloc); var primary = usable_store.loadPrimary(alloc) catch |err| blk: { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = diagnosticCauseForUserStoreError(err), }); break :blk settings_store.PrimaryLoad.absent; }; defer primary.deinit(alloc); switch (primary) { .absent => {}, .invalid => { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = .malformed_settings }); if (usable_store.newestValidBackupPath(alloc) catch null) |recovery_path| { try diagnostics.append(alloc, .{ .layer = .user, .cause = .manual_backup_available, .recovery_path = recovery_path, }); } }, .oversized => { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = .settings_too_large }); if (usable_store.newestValidBackupPath(alloc) catch null) |recovery_path| { try diagnostics.append(alloc, .{ .layer = .user, .cause = .manual_backup_available, .recovery_path = recovery_path, }); } }, .valid => |bytes| { user_root = try std.json.parseFromSlice(std.json.Value, alloc, bytes, .{ .allocate = .alloc_always }); if (user_root.?.value != .object) { prompt_history_store_allowed = false; try diagnostics.append(alloc, .{ .layer = .user, .cause = .malformed_settings }); user_root.?.deinit(); user_root = null; } }, } } } const project_path = try std.fs.path.join(alloc, &.{ workspace_root, ".fx.json" }); defer alloc.free(project_path); const project_text = readOptionalFile(alloc, project_path) catch |err| blk: { if (err == error.OutOfMemory) return err; try diagnostics.append(alloc, .{ .layer = .project, .cause = switch (err) { error.StreamTooLong => .settings_too_large, error.DurablePathUnsafe => .durable_path_unsafe, else => .malformed_settings, }, }); break :blk null; }; defer if (project_text) |text| alloc.free(text); if (project_text) |text| { if (std.json.parseFromSlice(std.json.Value, alloc, text, .{})) |parsed_value| { var parsed = parsed_value; defer parsed.deinit(); if (parsed.value != .object) { try diagnostics.append(alloc, .{ .layer = .project, .cause = .malformed_settings }); } else { try appendIgnoredProjectProfileSettingDiagnostics(alloc, &diagnostics, parsed.value); try mergeDetailedSettingsLayer( alloc, &detailed_merge_state, parsed.value, .project, false, .project, .project, .none, ); } } else |err| { if (err == error.OutOfMemory) return err; try diagnostics.append(alloc, .{ .layer = .project, .cause = .malformed_settings }); } } if (user_root) |parsed| { if (parsed.value.object.contains("additional_directories")) { try diagnostics.append(alloc, .{ .layer = .user, .cause = .invalid_additional_directories, .setting_key = try alloc.dupe(u8, "additional_directories"), }); } if (hasLegacyWorkspacePreferences(parsed.value)) { try diagnostics.append(alloc, .{ .layer = .user, .cause = .legacy_workspace_preferences, }); } try mergeDetailedSettingsLayer( alloc, &detailed_merge_state, parsed.value, .profile, false, .user, .user_global, .user, ); } if (user_root) |parsed| { const workspaces = parsed.value.object.get("workspaces"); if (workspaces) |workspaces_value| { if (workspaces_value != .object) { try diagnostics.append(alloc, .{ .layer = .user, .cause = .malformed_settings }); } else if (workspaces_value.object.get(workspace_root)) |workspace_value| { if (workspace_value != .object) { try diagnostics.append(alloc, .{ .layer = .user, .cause = .malformed_settings }); } else { if (workspace_value.object.get("additional_directories")) |value| { const parsed_directories = parseAdditionalDirectories( alloc, workspace_root, value, ) catch |err| blk: { if (err == error.OutOfMemory) return err; try diagnostics.append(alloc, .{ .layer = .user, .cause = .invalid_additional_directories, .setting_key = try alloc.dupe(u8, "additional_directories"), }); break :blk null; }; if (parsed_directories) |directories| { additional_directories = directories.canonical; additional_directory_sources = directories.sources; } } try mergeDetailedSettingsLayer( alloc, &detailed_merge_state, workspace_value, .profile, true, .user, .user_workspace, .local, ); } } } } if (io_mod.getenv("FX_MODEL")) |model_override| { if (std.mem.trim(u8, model_override, " \t\r\n").len > 0) { sources.model = .process_override; } } return .{ .settings = settings, .diagnostics = try diagnostics.toOwnedSlice(alloc), .model_source = sources.model, .sources = sources, .permission_sources = permission_sources, .prompt_history_store_allowed = prompt_history_store_allowed, .additional_directories = additional_directories, .additional_directory_sources = additional_directory_sources, }; } const ParsedAdditionalDirectories = struct { canonical: [][]u8, sources: [][]u8, }; fn parseAdditionalDirectories( alloc: Allocator, workspace_root: []const u8, value: std.json.Value, ) !ParsedAdditionalDirectories { if (value != .array) return error.InvalidAdditionalDirectories; var raw_paths: std.ArrayList([]const u8) = .empty; defer raw_paths.deinit(alloc); for (value.array.items) |item| { if (item != .string) return error.InvalidAdditionalDirectories; if (raw_paths.items.len >= workspace_access.max_additional_directories) { return error.InvalidAdditionalDirectories; } for (raw_paths.items) |path| { if (std.mem.eql(u8, path, item.string)) return error.InvalidAdditionalDirectories; } try raw_paths.append(alloc, item.string); } var access = workspace_access.WorkspaceAccess.init( alloc, workspace_root, raw_paths.items, &.{}, false, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.InvalidAdditionalDirectories, }; defer access.deinit(alloc); const canonical = try access.savedDirectoriesAlloc(alloc); errdefer freeStringSlice(alloc, canonical); return .{ .canonical = canonical,