Skip to content

Commit 7a5843c

Browse files
feat: add salvage functionality for security-sensitive values in GlobalContext
1 parent 187d445 commit 7a5843c

File tree

2 files changed

+177
-25
lines changed

2 files changed

+177
-25
lines changed

core/util/GlobalContext.test.ts

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe("GlobalContext", () => {
115115

116116
expect(consoleWarnMock).toHaveBeenCalledWith(
117117
expect.stringContaining(
118-
"Error updating global context, deleting corrupted file",
118+
"Error updating global context, attempting to salvage security-sensitive values",
119119
),
120120
);
121121

@@ -174,4 +174,154 @@ describe("GlobalContext", () => {
174174
it("should not crash or throw when getting a key that hasn't been set", () => {
175175
expect(globalContext.get("hasAlreadyCreatedAPromptFile")).toBeUndefined();
176176
});
177+
178+
describe("salvage functionality", () => {
179+
it("should salvage allowAnonymousTelemetry from corrupted sharedConfig", () => {
180+
// Create a file with partially corrupted JSON that contains salvageable sharedConfig
181+
const corruptedContent = `{
182+
"indexingPaused": true,
183+
"sharedConfig": {"allowAnonymousTelemetry": false},
184+
"corrupted": { invalid json
185+
}`;
186+
fs.writeFileSync(globalContextFilePath, corruptedContent);
187+
188+
const consoleWarnMock = jest
189+
.spyOn(console, "warn")
190+
.mockImplementation(() => {});
191+
192+
// Try to update - this should salvage the allowAnonymousTelemetry value
193+
globalContext.update("indexingPaused", false);
194+
195+
expect(consoleWarnMock).toHaveBeenCalledWith(
196+
expect.stringContaining(
197+
"Error updating global context, attempting to salvage security-sensitive values",
198+
),
199+
);
200+
201+
// Should have the new value
202+
expect(globalContext.get("indexingPaused")).toBe(false);
203+
204+
// Should have salvaged the sharedConfig with allowAnonymousTelemetry
205+
const salvaged = globalContext.get("sharedConfig");
206+
expect(salvaged).toEqual({ allowAnonymousTelemetry: false });
207+
208+
consoleWarnMock.mockRestore();
209+
});
210+
211+
it("should handle salvage when no sharedConfig is present", () => {
212+
const corruptedContent = `{
213+
"indexingPaused": true,
214+
"corrupted": { invalid json
215+
}`;
216+
fs.writeFileSync(globalContextFilePath, corruptedContent);
217+
218+
const consoleWarnMock = jest
219+
.spyOn(console, "warn")
220+
.mockImplementation(() => {});
221+
222+
globalContext.update("hasAlreadyCreatedAPromptFile", true);
223+
224+
expect(consoleWarnMock).toHaveBeenCalledWith(
225+
expect.stringContaining(
226+
"Error updating global context, attempting to salvage security-sensitive values",
227+
),
228+
);
229+
230+
// Should have the new value
231+
expect(globalContext.get("hasAlreadyCreatedAPromptFile")).toBe(true);
232+
233+
// Should not have any sharedConfig since none was salvageable
234+
expect(globalContext.get("sharedConfig")).toBeUndefined();
235+
236+
consoleWarnMock.mockRestore();
237+
});
238+
239+
it("should handle salvage when sharedConfig has invalid allowAnonymousTelemetry", () => {
240+
const corruptedContent = `{
241+
"indexingPaused": true,
242+
"sharedConfig": {"allowAnonymousTelemetry": "not-a-boolean"},
243+
"corrupted": { invalid json
244+
}`;
245+
fs.writeFileSync(globalContextFilePath, corruptedContent);
246+
247+
const consoleWarnMock = jest
248+
.spyOn(console, "warn")
249+
.mockImplementation(() => {});
250+
251+
globalContext.update("indexingPaused", false);
252+
253+
// Should not have salvaged an invalid boolean value
254+
const salvaged = globalContext.get("sharedConfig");
255+
expect(salvaged).toBeUndefined();
256+
257+
consoleWarnMock.mockRestore();
258+
});
259+
260+
it("should handle salvage with valid allowAnonymousTelemetry set to true", () => {
261+
const corruptedContent = `{
262+
"indexingPaused": true,
263+
"sharedConfig": {"allowAnonymousTelemetry": true, "otherField": "value"},
264+
"corrupted": { invalid json
265+
}`;
266+
fs.writeFileSync(globalContextFilePath, corruptedContent);
267+
268+
const consoleWarnMock = jest
269+
.spyOn(console, "warn")
270+
.mockImplementation(() => {});
271+
272+
globalContext.update("hasAlreadyCreatedAPromptFile", true);
273+
274+
// Should have salvaged only the allowAnonymousTelemetry value
275+
const salvaged = globalContext.get("sharedConfig");
276+
expect(salvaged).toEqual({ allowAnonymousTelemetry: true });
277+
278+
consoleWarnMock.mockRestore();
279+
});
280+
281+
it("should handle completely unparseable content gracefully", () => {
282+
fs.writeFileSync(
283+
globalContextFilePath,
284+
"complete garbage not json at all",
285+
);
286+
287+
const consoleWarnMock = jest
288+
.spyOn(console, "warn")
289+
.mockImplementation(() => {});
290+
291+
globalContext.update("indexingPaused", true);
292+
293+
// Should still work and create a new file
294+
expect(globalContext.get("indexingPaused")).toBe(true);
295+
expect(globalContext.get("sharedConfig")).toBeUndefined();
296+
297+
consoleWarnMock.mockRestore();
298+
});
299+
300+
it("should preserve multiple salvageable values when updating different key", () => {
301+
const corruptedContent = `{
302+
"indexingPaused": true,
303+
"sharedConfig": {"allowAnonymousTelemetry": false},
304+
"hasAlreadyCreatedAPromptFile": true,
305+
"corrupted": { invalid json
306+
}`;
307+
fs.writeFileSync(globalContextFilePath, corruptedContent);
308+
309+
const consoleWarnMock = jest
310+
.spyOn(console, "warn")
311+
.mockImplementation(() => {});
312+
313+
// Update a different key - should salvage and preserve existing data
314+
globalContext.update("showConfigUpdateToast", false);
315+
316+
// Should have the new value
317+
expect(globalContext.get("showConfigUpdateToast")).toBe(false);
318+
319+
// Should have salvaged the security-sensitive value
320+
expect(globalContext.get("sharedConfig")).toEqual({
321+
allowAnonymousTelemetry: false,
322+
});
323+
324+
consoleWarnMock.mockRestore();
325+
});
326+
});
177327
});

core/util/GlobalContext.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ import {
99
SharedConfigSchema,
1010
} from "../config/sharedConfig";
1111

12-
import {
13-
OAuthClientInformationFull,
14-
OAuthTokens,
15-
} from "@modelcontextprotocol/sdk/shared/auth.js";
1612
import { getGlobalContextFilePath } from "./paths";
1713

1814
export type GlobalContextModelSelections = Partial<
@@ -43,14 +39,6 @@ export type GlobalContextType = {
4339
isSupportedLanceDbCpuTargetForLinux: boolean;
4440
sharedConfig: SharedConfigSchema;
4541
failedDocs: SiteIndexingConfig[];
46-
mcpOauthStorage: Record<
47-
string,
48-
{
49-
clientInformation?: OAuthClientInformationFull;
50-
tokens?: OAuthTokens;
51-
codeVerifier?: string;
52-
}
53-
>;
5442
shownDeprecatedProviderWarnings: {
5543
[providerTitle: string]: boolean;
5644
};
@@ -84,8 +72,25 @@ export class GlobalContext {
8472
parsed = JSON.parse(data);
8573
} catch (e: any) {
8674
console.warn(
87-
`Error updating global context, deleting corrupted file: ${e}`,
75+
`Error updating global context, attempting to salvage security-sensitive values: ${e}`,
8876
);
77+
78+
// Attempt to salvage security-sensitive values before deleting
79+
let salvaged: Partial<GlobalContextType> = {};
80+
try {
81+
// Try to partially parse the corrupted data to extract sharedConfig
82+
const match = data.match(/"sharedConfig"\s*:\s*({[^}]*})/);
83+
if (match) {
84+
const sharedConfigObj = JSON.parse(match[1]);
85+
const salvagedSharedConfig = salvageSharedConfig(sharedConfigObj);
86+
if (Object.keys(salvagedSharedConfig).length > 0) {
87+
salvaged.sharedConfig = salvagedSharedConfig;
88+
}
89+
}
90+
} catch {
91+
// If salvage fails, continue with empty salvaged object
92+
}
93+
8994
// Delete the corrupted file and recreate it fresh
9095
try {
9196
fs.unlinkSync(filepath);
@@ -94,17 +99,14 @@ export class GlobalContext {
9499
`Error deleting corrupted global context file: ${deleteError}`,
95100
);
96101
}
97-
// Recreate the file with just the new value
98-
fs.writeFileSync(
99-
filepath,
100-
JSON.stringify(
101-
{
102-
[key]: value,
103-
},
104-
null,
105-
2,
106-
),
107-
);
102+
103+
// Recreate the file with salvaged values plus the new value
104+
const newData = {
105+
...salvaged,
106+
[key]: value,
107+
};
108+
109+
fs.writeFileSync(filepath, JSON.stringify(newData, null, 2));
108110
return;
109111
}
110112

0 commit comments

Comments
 (0)