

















@@ -23,6 +23,7 @@ let readExecApprovalsSnapshot: ExecApprovalsModule["readExecApprovalsSnapshot"];
2323let recordAllowlistMatchesUse: ExecApprovalsModule["recordAllowlistMatchesUse"];
2424let recordAllowlistUse: ExecApprovalsModule["recordAllowlistUse"];
2525let requestExecApprovalViaSocket: ExecApprovalsModule["requestExecApprovalViaSocket"];
26+let resolveExecApprovals: ExecApprovalsModule["resolveExecApprovals"];
2627let resolveExecApprovalsPath: ExecApprovalsModule["resolveExecApprovalsPath"];
2728let resolveExecApprovalsSocketPath: ExecApprovalsModule["resolveExecApprovalsSocketPath"];
2829let saveExecApprovals: ExecApprovalsModule["saveExecApprovals"];
@@ -42,6 +43,7 @@ beforeAll(async () => {
4243 recordAllowlistMatchesUse,
4344 recordAllowlistUse,
4445 requestExecApprovalViaSocket,
46+ resolveExecApprovals,
4547 resolveExecApprovalsPath,
4648 resolveExecApprovalsSocketPath,
4749 saveExecApprovals,
@@ -187,6 +189,143 @@ describe("exec approvals store helpers", () => {
187189expect(readApprovalsFile(dir).socket).toEqual(ensured.socket);
188190});
189191192+it("does not create an approvals file when resolving the missing default no-prompt policy", () => {
193+const dir = createHomeDir();
194+195+const resolved = resolveExecApprovals("main", {
196+security: "full",
197+ask: "off",
198+});
199+200+expect(resolved.agent.security).toBe("full");
201+expect(resolved.agent.ask).toBe("off");
202+expect(resolved.socketPath).toBe(resolveExecApprovalsSocketPath());
203+expect(resolved.token).toBe("");
204+expect(fs.existsSync(approvalsFilePath(dir))).toBe(false);
205+});
206+207+it("does not rewrite an empty approvals file for the default no-prompt policy", () => {
208+const dir = createHomeDir();
209+const approvalsPath = approvalsFilePath(dir);
210+fs.mkdirSync(path.dirname(approvalsPath), { recursive: true });
211+fs.writeFileSync(approvalsPath, "", "utf8");
212+213+const resolved = resolveExecApprovals("main", {
214+security: "full",
215+ask: "off",
216+});
217+218+expect(resolved.agent.security).toBe("full");
219+expect(resolved.agent.ask).toBe("off");
220+expect(resolved.token).toBe("");
221+expect(fs.statSync(approvalsPath).size).toBe(0);
222+});
223+224+it.runIf(process.platform !== "win32")(
225+"hardens existing token-bearing approvals files before resolving default no-prompt policy",
226+() => {
227+const dir = createHomeDir();
228+const approvalsPath = approvalsFilePath(dir);
229+fs.mkdirSync(path.dirname(approvalsPath), { recursive: true });
230+fs.writeFileSync(
231+approvalsPath,
232+JSON.stringify({
233+version: 1,
234+socket: { path: resolveExecApprovalsSocketPath(), token: "existing-token" },
235+defaults: { security: "full", ask: "off" },
236+agents: {},
237+}),
238+{ mode: 0o644 },
239+);
240+fs.chmodSync(approvalsPath, 0o644);
241+242+const resolved = resolveExecApprovals("main", {
243+security: "full",
244+ask: "off",
245+});
246+247+expect(resolved.agent.security).toBe("full");
248+expect(resolved.agent.ask).toBe("off");
249+expect(resolved.token).toBe("existing-token");
250+expect(fs.statSync(approvalsPath).mode & 0o777).toBe(0o600);
251+},
252+);
253+254+it.runIf(process.platform !== "win32")(
255+"rejects symlinked approvals files before resolving the default no-prompt policy",
256+() => {
257+const dir = createHomeDir();
258+const approvalsPath = approvalsFilePath(dir);
259+const linkedPath = path.join(dir, "linked-approvals.json");
260+fs.mkdirSync(path.dirname(approvalsPath), { recursive: true });
261+fs.writeFileSync(
262+linkedPath,
263+JSON.stringify({
264+version: 1,
265+defaults: { security: "full", ask: "off" },
266+agents: {},
267+}),
268+"utf8",
269+);
270+fs.symlinkSync(linkedPath, approvalsPath);
271+272+expect(() =>
273+resolveExecApprovals("main", {
274+security: "deny",
275+ask: "always",
276+}),
277+).toThrow("Refusing to write exec approvals via symlink");
278+},
279+);
280+281+it("does not treat approvals path access errors as a missing default policy", () => {
282+const dir = createHomeDir();
283+const approvalsPath = approvalsFilePath(dir);
284+const actualReadFileSync = fs.readFileSync.bind(fs);
285+vi.spyOn(fs, "readFileSync").mockImplementation((target, options) => {
286+if (String(target) === approvalsPath) {
287+throw Object.assign(new Error("approval path blocked"), { code: "EACCES" });
288+}
289+return actualReadFileSync(target, options as never);
290+});
291+292+expect(() =>
293+resolveExecApprovals("main", {
294+security: "full",
295+ask: "off",
296+}),
297+).toThrow("approval path blocked");
298+});
299+300+it("creates an approvals file when resolving a missing policy that may prompt", () => {
301+const dir = createHomeDir();
302+303+const resolved = resolveExecApprovals("main", {
304+security: "allowlist",
305+ask: "on-miss",
306+});
307+308+expect(resolved.agent.security).toBe("allowlist");
309+expect(resolved.agent.ask).toBe("on-miss");
310+expect(resolved.token).toMatch(/^[A-Za-z0-9_-]{32}$/);
311+expect(readApprovalsFile(dir).socket).toEqual(resolved.file.socket);
312+});
313+314+it("creates an approvals file for default no-prompt policy when a socket is required", () => {
315+const dir = createHomeDir();
316+317+const resolved = resolveExecApprovals("main", {
318+security: "full",
319+ask: "off",
320+requireSocket: true,
321+});
322+323+expect(resolved.agent.security).toBe("full");
324+expect(resolved.agent.ask).toBe("off");
325+expect(resolved.token).toMatch(/^[A-Za-z0-9_-]{32}$/);
326+expect(readApprovalsFile(dir).socket).toEqual(resolved.file.socket);
327+});
328+190329it("atomically replaces existing approvals files instead of mutating linked inodes", () => {
191330const dir = createHomeDir();
192331const approvalsPath = approvalsFilePath(dir);
@@ -236,6 +375,24 @@ describe("exec approvals store helpers", () => {
236375expect(fs.statSync(approvalsDir).mode & 0o777).toBe(0o700);
237376});
238377378+it.runIf(process.platform !== "win32")(
379+"keeps exec approvals strict when directory chmod fails",
380+() => {
381+const dir = createHomeDir();
382+const approvalsDir = path.dirname(approvalsFilePath(dir));
383+const actualChmodSync = fs.chmodSync.bind(fs);
384+vi.spyOn(fs, "chmodSync").mockImplementation((target, mode) => {
385+if (String(target) === approvalsDir) {
386+throw Object.assign(new Error("chmod denied"), { code: "EPERM" });
387+}
388+return actualChmodSync(target, mode);
389+});
390+391+expect(() => ensureExecApprovals()).toThrow("chmod denied");
392+expect(fs.existsSync(approvalsFilePath(dir))).toBe(false);
393+},
394+);
395+239396it("falls back to copying when rename cannot overwrite the approvals file", () => {
240397const dir = createHomeDir();
241398const approvalsPath = approvalsFilePath(dir);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。