Skip to content

Commit 6f9e167

Browse files
authored
Merge pull request #7157 from continuedev/feat/global-context-salvage-2
feat: add salvage functionality for security-sensitive values in Glob…
2 parents 190a1d1 + f1e1f6f commit 6f9e167

File tree

2 files changed

+187
-24
lines changed

2 files changed

+187
-24
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: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import fs from "node:fs";
22

33
import { ModelRole } from "@continuedev/config-yaml";
4+
import {
5+
OAuthClientInformationFull,
6+
OAuthTokens,
7+
} from "@modelcontextprotocol/sdk/shared/auth.js";
48

59
import { SiteIndexingConfig } from "..";
610
import {
@@ -9,10 +13,6 @@ import {
913
SharedConfigSchema,
1014
} from "../config/sharedConfig";
1115

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

1818
export type GlobalContextModelSelections = Partial<
@@ -43,16 +43,15 @@ export type GlobalContextType = {
4343
isSupportedLanceDbCpuTargetForLinux: boolean;
4444
sharedConfig: SharedConfigSchema;
4545
failedDocs: SiteIndexingConfig[];
46-
mcpOauthStorage: Record<
47-
string,
48-
{
46+
shownDeprecatedProviderWarnings: {
47+
[providerTitle: string]: boolean;
48+
};
49+
mcpOauthStorage: {
50+
[serverUrl: string]: {
4951
clientInformation?: OAuthClientInformationFull;
5052
tokens?: OAuthTokens;
5153
codeVerifier?: string;
52-
}
53-
>;
54-
shownDeprecatedProviderWarnings: {
55-
[providerTitle: string]: boolean;
54+
};
5655
};
5756
};
5857

@@ -84,8 +83,25 @@ export class GlobalContext {
8483
parsed = JSON.parse(data);
8584
} catch (e: any) {
8685
console.warn(
87-
`Error updating global context, deleting corrupted file: ${e}`,
86+
`Error updating global context, attempting to salvage security-sensitive values: ${e}`,
8887
);
88+
89+
// Attempt to salvage security-sensitive values before deleting
90+
let salvaged: Partial<GlobalContextType> = {};
91+
try {
92+
// Try to partially parse the corrupted data to extract sharedConfig
93+
const match = data.match(/"sharedConfig"\s*:\s*({[^}]*})/);
94+
if (match) {
95+
const sharedConfigObj = JSON.parse(match[1]);
96+
const salvagedSharedConfig = salvageSharedConfig(sharedConfigObj);
97+
if (Object.keys(salvagedSharedConfig).length > 0) {
98+
salvaged.sharedConfig = salvagedSharedConfig;
99+
}
100+
}
101+
} catch {
102+
// If salvage fails, continue with empty salvaged object
103+
}
104+
89105
// Delete the corrupted file and recreate it fresh
90106
try {
91107
fs.unlinkSync(filepath);
@@ -94,17 +110,14 @@ export class GlobalContext {
94110
`Error deleting corrupted global context file: ${deleteError}`,
95111
);
96112
}
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-
);
113+
114+
// Recreate the file with salvaged values plus the new value
115+
const newData = {
116+
...salvaged,
117+
[key]: value,
118+
};
119+
120+
fs.writeFileSync(filepath, JSON.stringify(newData, null, 2));
108121
return;
109122
}
110123

0 commit comments

Comments
 (0)