Skip to content

Commit 1b22fa2

Browse files
Merge pull request #513 from corbitsdev/cl-6707-resolve-project-trust-plugin-paths-against-the-project-cwd
Resolve project trust plugin paths against the project cwd
2 parents 341b3ad + 54d7ee8 commit 1b22fa2

2 files changed

Lines changed: 69 additions & 5 deletions

File tree

‎src/trust/project-trust.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { dirname, join } from "node:path";
55

6+
import { resolve } from "node:path";
7+
68
import {
9+
isPluginTrusted,
710
loadProjectTrust,
811
projectTrustPath,
912
readProjectTrustStore,
@@ -117,6 +120,44 @@ describe("project trust store", () => {
117120
});
118121
});
119122

123+
test("a relative grant resolves against the project cwd, not process.cwd()", async () => {
124+
// process.cwd() during test runs is the repo checkout, not the project
125+
// directory under test — a real-world stand-in for "some other tree".
126+
expect(process.cwd()).not.toBe("/repo/under/test");
127+
await withTempHome(async (home, cwd) => {
128+
await trustPlugin(cwd, "relative/plugin", home);
129+
130+
const store = await loadProjectTrust(cwd, home);
131+
expect(store.trustedPluginPaths).toEqual([resolve(cwd, "relative/plugin")]);
132+
133+
// The grant binds to the project cwd's tree...
134+
expect(isPluginTrusted(store, "relative/plugin", cwd)).toBe(true);
135+
// ...not to process.cwd()'s tree, even though it resolves the same
136+
// relative string.
137+
expect(isPluginTrusted(store, "relative/plugin", process.cwd())).toBe(false);
138+
expect(isPluginTrusted(store, resolve(process.cwd(), "relative/plugin"))).toBe(false);
139+
});
140+
});
141+
142+
test("a non-absolute trustedPluginPaths entry on disk is dropped on load, not resolved against process.cwd()", async () => {
143+
await withTempHome(async (home, cwd) => {
144+
const path = projectTrustPath(cwd, home);
145+
await mkdir(dirname(path), { recursive: true });
146+
await writeFile(
147+
path,
148+
JSON.stringify({
149+
repo: cwd,
150+
trustedPluginPaths: ["relative/plugin", "/plugins/absolute"],
151+
trustedMcpFingerprints: [],
152+
}),
153+
);
154+
155+
const result = await readProjectTrustStore(cwd, home);
156+
expect(result.state).toBe("valid");
157+
expect(result.store.trustedPluginPaths).toEqual(["/plugins/absolute"]);
158+
});
159+
});
160+
120161
test("store with a non-string repo field is invalid", async () => {
121162
await withTempHome(async (home, cwd) => {
122163
const path = projectTrustPath(cwd, home);

‎src/trust/project-trust.ts‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
22
import { homedir } from "node:os";
3-
import { dirname, join, resolve } from "node:path";
3+
import { dirname, isAbsolute, join, resolve } from "node:path";
44
import { createHash } from "node:crypto";
55
import { type } from "arktype";
66
import { getLogger } from "@intx/log";
@@ -142,10 +142,22 @@ export async function readProjectTrustStore(
142142
logger.warn`project trust store repo mismatch at ${path}: recorded ${validated.repo}, expected ${resolve(cwd)}`;
143143
return { state: "invalid", store: emptyStore() };
144144
}
145+
// Grants are recorded as absolute paths (see requireAbsolute below); a
146+
// relative entry has no fixed meaning on load — resolving it here would
147+
// bind to whatever process.cwd() happens to be, the same confused-cwd bug
148+
// path-trust.ts guards against on disk. Drop it instead of guessing.
149+
const absolutePluginPaths: string[] = [];
150+
for (const p of trustedPluginPaths) {
151+
if (!isAbsolute(p)) {
152+
logger.warn`project trust store dropping non-absolute trustedPluginPaths entry at ${path}: ${p}`;
153+
continue;
154+
}
155+
absolutePluginPaths.push(resolve(p));
156+
}
145157
return {
146158
state: "valid",
147159
store: {
148-
trustedPluginPaths: trustedPluginPaths.map((p) => resolve(p)),
160+
trustedPluginPaths: absolutePluginPaths,
149161
trustedMcpFingerprints: [...trustedMcpFingerprints],
150162
},
151163
};
@@ -185,8 +197,19 @@ function enqueueMutation<T>(key: string, run: () => Promise<T>): Promise<T> {
185197
return next;
186198
}
187199

188-
export function isPluginTrusted(store: ProjectTrustStore, pluginPath: string): boolean {
189-
const abs = resolve(pluginPath);
200+
// A relative pluginPath has no fixed meaning until resolved against some cwd;
201+
// resolving it against process.cwd() (path.resolve's default) would trust a
202+
// different directory than the caller's project, the confused-cwd bug
203+
// path-trust.ts avoids by requiring absolute paths outright. Project trust
204+
// callers pass relative paths in practice, so resolve against the given
205+
// project cwd instead of rejecting — path.resolve(cwd, pluginPath) leaves an
206+
// already-absolute pluginPath untouched.
207+
function resolveAgainstProjectCwd(cwd: string, pluginPath: string): string {
208+
return resolve(cwd, pluginPath);
209+
}
210+
211+
export function isPluginTrusted(store: ProjectTrustStore, pluginPath: string, cwd: string = process.cwd()): boolean {
212+
const abs = resolveAgainstProjectCwd(cwd, pluginPath);
190213
return store.trustedPluginPaths.includes(abs);
191214
}
192215

@@ -195,7 +218,7 @@ export async function trustPlugin(
195218
pluginPath: string,
196219
home: string = homedir(),
197220
): Promise<ProjectTrustStore> {
198-
const abs = resolve(pluginPath);
221+
const abs = resolveAgainstProjectCwd(cwd, pluginPath);
199222
return enqueueMutation(projectTrustPath(cwd, home), async () => {
200223
const store = await loadProjectTrust(cwd, home);
201224
if (!store.trustedPluginPaths.includes(abs)) {

0 commit comments

Comments
 (0)