From 489a3ef2b8edc2e11c08c108e39681efd55599d0 Mon Sep 17 00:00:00 2001 From: jackwener Date: Wed, 24 Jun 2026 03:10:56 +0800 Subject: [PATCH] Fail closed on invalid telemetry files --- .../src/__tests__/telemetry-repo.test.ts | 52 +++++++++++++++++++ packages/storage/src/telemetry-repo.ts | 26 ++++++++-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/storage/src/__tests__/telemetry-repo.test.ts b/packages/storage/src/__tests__/telemetry-repo.test.ts index 3df1624998..8986bc2418 100644 --- a/packages/storage/src/__tests__/telemetry-repo.test.ts +++ b/packages/storage/src/__tests__/telemetry-repo.test.ts @@ -180,6 +180,22 @@ describe('FileTelemetryRepo', () => { } }); + test('load accepts legacy telemetry files with only known array sections', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-telemetry-legacy-')); + try { + await writeFile(join(root, 'telemetry.json'), JSON.stringify({ usageRecords: [] }) + '\n', 'utf8'); + const repo = createTelemetryRepo(root); + + await repo.load(); + + assert.deepEqual(repo.logs({ range: 'all' }), { rows: [], total: 0 }); + assert.deepEqual(repo.listPricingOverrides(), []); + } finally { + await flushWrites(); + await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }); + } + }); + test('load rejects corrupt telemetry.json without overwriting usage history bytes', async () => { const root = await mkdtemp(join(tmpdir(), 'maka-telemetry-corrupt-')); try { @@ -194,6 +210,42 @@ describe('FileTelemetryRepo', () => { await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }); } }); + + test('load rejects wrong telemetry schema without overwriting bytes', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-telemetry-wrong-schema-')); + try { + const wrongShape = JSON.stringify({ reminders: [] }, null, 2) + '\n'; + await writeFile(join(root, 'telemetry.json'), wrongShape, 'utf8'); + const repo = createTelemetryRepo(root); + + await assert.rejects( + () => repo.load(), + /expected known telemetry sections/, + ); + assert.equal(await readFile(join(root, 'telemetry.json'), 'utf8'), wrongShape); + } finally { + await flushWrites(); + await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }); + } + }); + + test('load rejects known telemetry sections with non-array values', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-telemetry-bad-section-')); + try { + const wrongShape = JSON.stringify({ usageRecords: {} }, null, 2) + '\n'; + await writeFile(join(root, 'telemetry.json'), wrongShape, 'utf8'); + const repo = createTelemetryRepo(root); + + await assert.rejects( + () => repo.load(), + /usageRecords must be an array/, + ); + assert.equal(await readFile(join(root, 'telemetry.json'), 'utf8'), wrongShape); + } finally { + await flushWrites(); + await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }); + } + }); }); async function withRepo(fn: (repo: ReturnType) => Promise): Promise { diff --git a/packages/storage/src/telemetry-repo.ts b/packages/storage/src/telemetry-repo.ts index 29a19a5e76..dd65713198 100644 --- a/packages/storage/src/telemetry-repo.ts +++ b/packages/storage/src/telemetry-repo.ts @@ -234,15 +234,33 @@ function emptyFile(): TelemetryFile { } function normalizeFile(input: unknown): TelemetryFile { - if (!input || typeof input !== 'object') return emptyFile(); + if (!input || typeof input !== 'object' || Array.isArray(input)) { + throw new Error('Invalid telemetry file: expected an object'); + } const value = input as Partial; + const hasKnownSection = + 'usageRecords' in value || + 'toolInvocations' in value || + 'pricingOverrides' in value; + if (!hasKnownSection) { + throw new Error('Invalid telemetry file: expected known telemetry sections'); + } + assertOptionalArraySection(value, 'usageRecords'); + assertOptionalArraySection(value, 'toolInvocations'); + assertOptionalArraySection(value, 'pricingOverrides'); return { - usageRecords: Array.isArray(value.usageRecords) ? value.usageRecords.map(normalizeLlmCallRecord) : [], - toolInvocations: Array.isArray(value.toolInvocations) ? value.toolInvocations : [], - pricingOverrides: Array.isArray(value.pricingOverrides) ? value.pricingOverrides : [], + usageRecords: value.usageRecords ? value.usageRecords.map(normalizeLlmCallRecord) : [], + toolInvocations: value.toolInvocations ?? [], + pricingOverrides: value.pricingOverrides ?? [], }; } +function assertOptionalArraySection(value: T, key: keyof T): void { + if (key in value && !Array.isArray(value[key])) { + throw new Error(`Invalid telemetry file: ${String(key)} must be an array`); + } +} + function normalizeLlmCallRecord(input: unknown): PersistedLlmCallRecord { const row = input as Partial; const inputTokens = finiteNumber(row.inputTokens) ?? 0;