-
Notifications
You must be signed in to change notification settings - Fork 41
fix: avoid overwriting malformed global config #1339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto'; | |
| import { homedir } from 'os'; | ||
| import { join } from 'path'; | ||
| import { z } from 'zod'; | ||
| import { toError } from '../../errors/types.js'; | ||
|
|
||
| export const GLOBAL_CONFIG_DIR = process.env.AGENTCORE_CONFIG_DIR ?? join(homedir(), '.agentcore'); | ||
| export const GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, 'config.json'); | ||
|
|
@@ -46,20 +47,55 @@ export function readGlobalConfigSync(configFile = GLOBAL_CONFIG_FILE): GlobalCon | |
| } | ||
| } | ||
|
|
||
| export type UpdateGlobalConfigResult = { success: true } | { success: false; error: string }; | ||
|
|
||
| export async function updateGlobalConfig( | ||
| partial: GlobalConfig, | ||
| configDir = GLOBAL_CONFIG_DIR, | ||
| configFile = GLOBAL_CONFIG_FILE | ||
| ): Promise<boolean> { | ||
| try { | ||
| const existing = await readGlobalConfig(configFile); | ||
| const merged: GlobalConfig = mergeConfig(existing, partial); | ||
| ): Promise<UpdateGlobalConfigResult> { | ||
| // Read the existing config strictly: a missing file is fine (start fresh), but a | ||
| // malformed file must not be silently overwritten with merged-in defaults. | ||
| const existing = await loadConfigForUpdate(configFile); | ||
| if (!existing.success) { | ||
| return existing; | ||
| } | ||
|
|
||
| try { | ||
| const merged: GlobalConfig = mergeConfig(existing.config, partial); | ||
| await mkdir(configDir, { recursive: true }); | ||
| await writeFile(configFile, JSON.stringify(merged, null, 2), 'utf-8'); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| return { success: true }; | ||
| } catch (error) { | ||
| return { success: false, error: `Failed to write config to ${configFile}: ${toError(error).message}` }; | ||
| } | ||
| } | ||
|
|
||
| type LoadConfigResult = { success: true; config: GlobalConfig } | { success: false; error: string }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we call the field errorMsg or make it store the full error object? What you did is in-line with an existing codebase pattern I'm trying to undo where we maintain errors as strings everywhere. This makes sense for displaying to the user, but creates issues when we want programmatic logic based on errors (ex. check if downstream threw an error of type X). Without the object, we lose error type information and are forced to rely on brittle string matching to determine the cause of the error. |
||
|
|
||
| /** | ||
| * Reads the existing global config for an update. Distinguishes a missing file | ||
| * (treated as an empty config) from a malformed one (read/parse/schema failure), | ||
| * so the caller can avoid clobbering a config it could not understand. | ||
| */ | ||
| async function loadConfigForUpdate(configFile: string): Promise<LoadConfigResult> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this logic feels a bit convoluted to me. Do you think it makes sense to directly check if the file exists (via fs.exists or something), then use that instead of the error? |
||
| let data: string; | ||
| try { | ||
| data = await readFile(configFile, 'utf-8'); | ||
| } catch (error) { | ||
| if ((error as NodeJS.ErrnoException).code === 'ENOENT') { | ||
| return { success: true, config: {} }; | ||
| } | ||
| return { success: false, error: `Could not read config at ${configFile}: ${toError(error).message}` }; | ||
| } | ||
|
|
||
| try { | ||
| return { success: true, config: GlobalConfigSchema.parse(JSON.parse(data)) }; | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: `Config at ${configFile} is malformed and was left unchanged: ${toError(error).message}`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: i don't think we need the "left unchanged" |
||
| }; | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice simple test!