From bbe129d6fc255fee8ec94eeaabff5fc4b49eb5ef Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 22 Aug 2026 13:38:35 -0700 Subject: [PATCH] Treat missing project-trust repo field as invalid A store without repo skipped the mismatch guard entirely and was accepted as valid, applying its grants to whatever cwd hashed to that filename. --- src/trust/project-trust.test.ts | 33 +++++++++++++++++++++++++++++++-- src/trust/project-trust.ts | 11 +++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/trust/project-trust.test.ts b/src/trust/project-trust.test.ts index 3cb4718bb..81e649718 100644 --- a/src/trust/project-trust.test.ts +++ b/src/trust/project-trust.test.ts @@ -1,11 +1,12 @@ import { describe, test, expect } from "bun:test"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { loadProjectTrust, projectTrustPath, + readProjectTrustStore, trustMcpServer, trustPlugin, type ProjectTrustStore, @@ -99,4 +100,32 @@ describe("project trust store", () => { expect(exists).toBe(false); }); }); + + test("store without a repo field is invalid, not defaulted to empty-but-valid", async () => { + await withTempHome(async (home, cwd) => { + const path = projectTrustPath(cwd, home); + await mkdir(dirname(path), { recursive: true }); + await writeFile( + path, + JSON.stringify({ trustedPluginPaths: ["/plugins/a"], trustedMcpFingerprints: [] }), + ); + + const result = await readProjectTrustStore(cwd, home); + expect(result.state).toBe("invalid"); + expect(result.store.trustedPluginPaths).toEqual([]); + expect(result.store.trustedMcpFingerprints).toEqual([]); + }); + }); + + test("store with a non-string repo field is invalid", async () => { + await withTempHome(async (home, cwd) => { + const path = projectTrustPath(cwd, home); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, JSON.stringify({ repo: 12345, trustedPluginPaths: [] })); + + const result = await readProjectTrustStore(cwd, home); + expect(result.state).toBe("invalid"); + expect(result.store).toEqual({ trustedPluginPaths: [], trustedMcpFingerprints: [] }); + }); + }); }); diff --git a/src/trust/project-trust.ts b/src/trust/project-trust.ts index ee0911271..6d932195f 100644 --- a/src/trust/project-trust.ts +++ b/src/trust/project-trust.ts @@ -130,8 +130,15 @@ export async function readProjectTrustStore( path, ); // Guard against a stale/copied record keyed to a different repo path: the - // file records the repo it was written for and must match this cwd. - if (validated.repo !== undefined && resolve(validated.repo) !== resolve(cwd)) { + // file records the repo it was written for and must match this cwd. A + // missing or non-string `repo` is invalid too — without it, a hand-edited + // or stripped store would apply its grants to whatever cwd happens to hash + // to this filename, defeating the mismatch guard entirely. + if (typeof validated.repo !== "string") { + logger.warn`project trust store missing repo field at ${path}`; + return { state: "invalid", store: emptyStore() }; + } + if (resolve(validated.repo) !== resolve(cwd)) { logger.warn`project trust store repo mismatch at ${path}: recorded ${validated.repo}, expected ${resolve(cwd)}`; return { state: "invalid", store: emptyStore() }; }