Skip to content

Commit 6b1b77f

Browse files
committed
refactor(stores): harden settings store with union types, symmetric sanitization, numeric clamping, and default reset
1 parent ecd98d3 commit 6b1b77f

2 files changed

Lines changed: 109 additions & 34 deletions

File tree

src/stores/settings_store.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,50 @@ describe('settings_store', () => {
189189
expect(is_valid_api_key('tadpole-dev-token-2026')).toBe(true);
190190
expect(is_valid_api_key('tadpole-os-sidecar-default-2026')).toBe(true);
191191
});
192+
193+
it('applies symmetric sanitization in update_setting', async () => {
194+
const { get_settings, use_settings_store } = await import('./settings_store');
195+
await use_settings_store.persist.rehydrate();
196+
197+
// Updating api key with banned legacy token should be sanitized to empty string
198+
use_settings_store.getState().update_setting('tadpole_os_api_key', 'my-secure-token-123');
199+
expect(get_settings().tadpole_os_api_key).toBe('');
200+
201+
// Updating with trailing slashes in url should be trimmed
202+
use_settings_store.getState().update_setting('tadpole_os_url', ' http://custom:9000/// ');
203+
expect(get_settings().tadpole_os_url).toBe('http://custom:9000');
204+
});
205+
206+
it('enforces numeric bounds clamping on temperature and agents', async () => {
207+
const { get_settings, save_settings, use_settings_store } = await import('./settings_store');
208+
await use_settings_store.persist.rehydrate();
209+
210+
save_settings({
211+
...get_settings(),
212+
default_temperature: 5.5,
213+
max_agents: 9999,
214+
max_clusters: -10,
215+
});
216+
217+
expect(get_settings().default_temperature).toBe(2.0);
218+
expect(get_settings().max_agents).toBe(100);
219+
expect(get_settings().max_clusters).toBe(1);
220+
221+
// update_setting clamping
222+
use_settings_store.getState().update_setting('default_temperature', -1);
223+
expect(get_settings().default_temperature).toBe(0.0);
224+
});
225+
226+
it('resets settings to defaults with reset_to_defaults', async () => {
227+
const { get_settings, get_default_settings, reset_settings, use_settings_store } = await import('./settings_store');
228+
await use_settings_store.persist.rehydrate();
229+
230+
use_settings_store.getState().update_setting('theme', 'slate');
231+
expect(get_settings().theme).toBe('slate');
232+
233+
reset_settings();
234+
expect(get_settings().theme).toBe(get_default_settings().theme);
235+
});
192236
});
193237

194238

src/stores/settings_store.ts

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ const LEGACY_DEV_TOKENS = new Set([
3232
'my-secure-token-123',
3333
]);
3434

35+
export type ThemeOption = 'zinc' | 'slate' | 'neutral';
36+
export type DensityOption = 'compact' | 'comfortable';
37+
export type BackdropThemeOption = 'cyan' | 'emerald' | 'nebula' | 'slate' | 'amber';
38+
3539
export interface Tadpole_Settings {
3640
tadpole_os_url: string;
3741
tadpole_os_api_key: string;
38-
theme: string;
39-
density: string;
40-
backdrop_theme: string;
42+
theme: ThemeOption;
43+
density: DensityOption;
44+
backdrop_theme: BackdropThemeOption;
4145
default_model: string;
4246
default_temperature: number;
4347
auto_approve_safe_skills: boolean;
@@ -58,11 +62,12 @@ interface Settings_State {
5862
settings: Tadpole_Settings;
5963
save_settings: (new_settings: Tadpole_Settings) => string | null;
6064
update_setting: <K extends keyof Tadpole_Settings>(key: K, value: Tadpole_Settings[K]) => void;
65+
reset_to_defaults: () => void;
6166
}
6267

63-
const get_base_url = (): string => {
68+
export const get_base_url = (): string => {
6469
if (import.meta.env.VITE_TADPOLE_OS_URL) {
65-
return import.meta.env.VITE_TADPOLE_OS_URL;
70+
return import.meta.env.VITE_TADPOLE_OS_URL.trim().replace(/\/+$/, '');
6671
}
6772
// Dynamically align loopback URL with current window origin if available
6873
if (typeof window !== 'undefined' && window.location?.hostname) {
@@ -76,19 +81,20 @@ const get_base_url = (): string => {
7681

7782
const normalize_loopback_url = (url: string | undefined): string => {
7883
if (!url) return get_base_url();
79-
if (url.toLowerCase().includes('tauri')) {
84+
const cleaned = url.trim().replace(/\/+$/, '');
85+
if (cleaned.toLowerCase().includes('tauri')) {
8086
return get_base_url();
8187
}
8288
if (typeof window !== 'undefined' && window.location?.hostname) {
8389
const current_host = window.location.hostname;
84-
if (current_host === 'localhost' && url.includes('127.0.0.1:8000')) {
85-
return url.replace('127.0.0.1:8000', 'localhost:8000');
90+
if (current_host === 'localhost' && cleaned.includes('127.0.0.1:8000')) {
91+
return cleaned.replace('127.0.0.1:8000', 'localhost:8000');
8692
}
87-
if (current_host === '127.0.0.1' && url.includes('localhost:8000')) {
88-
return url.replace('localhost:8000', '127.0.0.1:8000');
93+
if (current_host === '127.0.0.1' && cleaned.includes('localhost:8000')) {
94+
return cleaned.replace('localhost:8000', '127.0.0.1:8000');
8995
}
9096
}
91-
return url;
97+
return cleaned;
9298
};
9399

94100
const sanitize_api_key = (value: string): string => {
@@ -102,7 +108,28 @@ const sanitize_settings = (settings: Tadpole_Settings): Tadpole_Settings => ({
102108
tadpole_os_api_key: sanitize_api_key(settings.tadpole_os_api_key || ''),
103109
});
104110

105-
111+
/** Canonical default configuration state */
112+
export const get_default_settings = (): Tadpole_Settings => ({
113+
tadpole_os_url: get_base_url(),
114+
tadpole_os_api_key: import.meta.env.VITE_NEURAL_TOKEN || '',
115+
theme: 'zinc',
116+
density: 'compact',
117+
backdrop_theme: 'cyan',
118+
default_model: 'GPT-4o',
119+
default_temperature: 0.7,
120+
auto_approve_safe_skills: true,
121+
max_agents: 50,
122+
max_clusters: 10,
123+
max_swarm_depth: 5,
124+
max_task_length: 32768,
125+
default_budget_usd: 1.0,
126+
is_safe_mode: true, // Default to safe mode for stabilization
127+
privacy_mode: false,
128+
browser_specialist_model_id: 'HuggingFaceTB/SmolLM-360M-Instruct',
129+
computer_architect_url: 'http://localhost:11434',
130+
enable_neural_handoff: true,
131+
sentinel_mode: false,
132+
});
106133

107134
/** is_valid_url - Validates a URL string for HTTP/HTTPS protocols. */
108135
export function is_valid_url(url: string): boolean {
@@ -127,27 +154,7 @@ export function is_valid_api_key(api_key: string): boolean {
127154
export const use_settings_store = create<Settings_State>()(
128155
persist(
129156
(set, get) => ({
130-
settings: {
131-
tadpole_os_url: get_base_url(),
132-
tadpole_os_api_key: import.meta.env.VITE_NEURAL_TOKEN || '',
133-
theme: 'zinc',
134-
density: 'compact',
135-
backdrop_theme: 'cyan',
136-
default_model: 'GPT-4o',
137-
default_temperature: 0.7,
138-
auto_approve_safe_skills: true,
139-
max_agents: 50,
140-
max_clusters: 10,
141-
max_swarm_depth: 5,
142-
max_task_length: 32768,
143-
default_budget_usd: 1.0,
144-
is_safe_mode: true, // Default to safe mode for stabilization
145-
privacy_mode: false,
146-
browser_specialist_model_id: 'HuggingFaceTB/SmolLM-360M-Instruct',
147-
computer_architect_url: 'http://localhost:11434',
148-
enable_neural_handoff: true,
149-
sentinel_mode: false,
150-
} as unknown as Tadpole_Settings,
157+
settings: get_default_settings(),
151158

152159
save_settings: (new_settings) => {
153160
// NUCLEAR PROTECTION: Never allow internal tauri URIs to be explicitly saved
@@ -161,11 +168,24 @@ export const use_settings_store = create<Settings_State>()(
161168
if (!is_valid_url(clean_url)) {
162169
return 'Invalid URL. Must start with http:// or https://';
163170
}
171+
172+
// Numeric Invariant Clamping
173+
const clamped_temperature = Math.min(2.0, Math.max(0.0, Number(new_settings.default_temperature) || 0.7));
174+
const clamped_agents = Math.min(100, Math.max(1, Math.floor(Number(new_settings.max_agents) || 50)));
175+
const clamped_clusters = Math.min(20, Math.max(1, Math.floor(Number(new_settings.max_clusters) || 10)));
176+
const clamped_swarm_depth = Math.min(10, Math.max(1, Math.floor(Number(new_settings.max_swarm_depth) || 5)));
177+
const clamped_budget = Math.max(0, Number(new_settings.default_budget_usd) || 0);
178+
164179
set({
165180
settings: {
166181
...new_settings,
167182
tadpole_os_url: clean_url,
168-
tadpole_os_api_key: sanitize_api_key(new_settings.tadpole_os_api_key),
183+
tadpole_os_api_key: sanitize_api_key(new_settings.tadpole_os_api_key || ''),
184+
default_temperature: clamped_temperature,
185+
max_agents: clamped_agents,
186+
max_clusters: clamped_clusters,
187+
max_swarm_depth: clamped_swarm_depth,
188+
default_budget_usd: clamped_budget,
169189
}
170190
});
171191
return null;
@@ -182,9 +202,19 @@ export const use_settings_store = create<Settings_State>()(
182202
} else {
183203
final_value = normalize_loopback_url(value) as unknown as Tadpole_Settings[K];
184204
}
205+
} else if (key === 'tadpole_os_api_key' && typeof value === 'string') {
206+
final_value = sanitize_api_key(value) as unknown as Tadpole_Settings[K];
207+
} else if (key === 'default_temperature' && typeof value === 'number') {
208+
final_value = Math.min(2.0, Math.max(0.0, value)) as unknown as Tadpole_Settings[K];
209+
} else if (key === 'max_agents' && typeof value === 'number') {
210+
final_value = Math.min(100, Math.max(1, Math.floor(value))) as unknown as Tadpole_Settings[K];
185211
}
186212

187213
set({ settings: { ...current, [key]: final_value } });
214+
},
215+
216+
reset_to_defaults: () => {
217+
set({ settings: get_default_settings() });
188218
}
189219
}),
190220
{
@@ -233,6 +263,7 @@ export const use_settings_store = create<Settings_State>()(
233263
// Backward compatibility helpers for non-reactive code
234264
export const get_settings = (): Tadpole_Settings => use_settings_store.getState().settings;
235265
export const save_settings = (s: Tadpole_Settings): string | null => use_settings_store.getState().save_settings(s);
266+
export const reset_settings = (): void => use_settings_store.getState().reset_to_defaults();
236267

237268

238269

0 commit comments

Comments
 (0)