refactor(permission-system): simplify policy and autoreview flow

This commit is contained in:
云服务部-叶林立
2026-09-03 02:01:06 +08:00
parent 180da95ab7
commit 6e7effa792
112 changed files with 911 additions and 10346 deletions
+11 -142
View File
@@ -245,7 +245,6 @@ describe("loadUnifiedConfig", () => {
"*": "ask",
read: "allow",
bash: { "*": "ask", "git *": "allow" },
external_directory: "ask",
},
}),
);
@@ -256,7 +255,6 @@ describe("loadUnifiedConfig", () => {
"*": "ask",
read: "allow",
bash: { "*": "ask", "git *": "allow" },
external_directory: "ask",
});
});
@@ -401,32 +399,6 @@ describe("loadUnifiedConfig", () => {
expect(result.issues.length).toBeGreaterThan(0);
});
it("parses piInfrastructureReadPaths when a valid string array is present", () => {
const configPath = join(tempDir, "config.json");
writeFileSync(
configPath,
JSON.stringify({ piInfrastructureReadPaths: ["/extra/path"] }),
);
const result = loadUnifiedConfig(configPath);
expect(result.config.piInfrastructureReadPaths).toEqual(["/extra/path"]);
});
it("parses piInfrastructureReadPaths as empty array when set to []", () => {
const configPath = join(tempDir, "config.json");
writeFileSync(
configPath,
JSON.stringify({ piInfrastructureReadPaths: [] }),
);
const result = loadUnifiedConfig(configPath);
expect(result.config.piInfrastructureReadPaths).toEqual([]);
});
it("omits piInfrastructureReadPaths when absent", () => {
const configPath = join(tempDir, "config.json");
writeFileSync(configPath, JSON.stringify({ debugLog: false }));
const result = loadUnifiedConfig(configPath);
expect(result.config).not.toHaveProperty("piInfrastructureReadPaths");
});
it.each([
["string", "not-an-array"],
@@ -446,7 +418,7 @@ describe("loadUnifiedConfig", () => {
});
describe("mergeUnifiedConfigs", () => {
it("deep-merges permission objects so project overrides global per-key", () => {
it("deep-merges permission objects so the later config overrides per key", () => {
const merged = mergeUnifiedConfigs(
{
permission: {
@@ -496,7 +468,7 @@ describe("mergeUnifiedConfigs", () => {
expect(merged.permission).toEqual({ bash: "deny" });
});
it("replaces scalar runtime knobs (project wins)", () => {
it("replaces scalar runtime knobs with later values", () => {
const merged = mergeUnifiedConfigs(
{
debugLog: true,
@@ -513,7 +485,7 @@ describe("mergeUnifiedConfigs", () => {
expect(merged.doublePressToConfirm).toBe(false);
});
it("replaces the prompt-budget scalars (project wins)", () => {
it("replaces prompt-budget scalars with later values", () => {
const merged = mergeUnifiedConfigs(
{ promptMaxRows: 24, promptFieldMaxWidth: 400 },
{ promptMaxRows: 10 },
@@ -523,7 +495,7 @@ describe("mergeUnifiedConfigs", () => {
expect(merged.promptFieldMaxWidth).toBe(400);
});
it("replaces the review-log field width (project wins)", () => {
it("replaces the review-log field width with a later value", () => {
expect(
mergeUnifiedConfigs(
{ reviewLogFieldMaxWidth: 1000 },
@@ -612,34 +584,6 @@ describe("mergeUnifiedConfigs", () => {
expect(merged).not.toHaveProperty("toolTextSummaryMaxLength");
});
it("override piInfrastructureReadPaths replaces base array", () => {
const merged = mergeUnifiedConfigs(
{ piInfrastructureReadPaths: ["/base/path"] },
{ piInfrastructureReadPaths: ["/override/path"] },
);
expect(merged.piInfrastructureReadPaths).toEqual(["/override/path"]);
});
it("base piInfrastructureReadPaths survives when override omits it", () => {
const merged = mergeUnifiedConfigs(
{ piInfrastructureReadPaths: ["/kept/path"] },
{ debugLog: true },
);
expect(merged.piInfrastructureReadPaths).toEqual(["/kept/path"]);
});
it("piInfrastructureReadPaths is absent when both base and override omit it", () => {
const merged = mergeUnifiedConfigs({ debugLog: true }, { yoloMode: false });
expect(merged).not.toHaveProperty("piInfrastructureReadPaths");
});
it("override piInfrastructureReadPaths as empty array replaces non-empty base", () => {
const merged = mergeUnifiedConfigs(
{ piInfrastructureReadPaths: ["/base/path"] },
{ piInfrastructureReadPaths: [] },
);
expect(merged.piInfrastructureReadPaths).toEqual([]);
});
it("override authorizerChain replaces base array", () => {
const merged = mergeUnifiedConfigs(
@@ -764,27 +708,6 @@ describe("loadAndMergeConfigs", () => {
writeFileSync(join(extensionRoot, "config.json"), JSON.stringify(content));
}
it("merges global and project new-layout configs", () => {
writeGlobal({
debugLog: true,
permission: { "*": "ask", read: "allow" },
});
writeProject({
permission: { "*": "allow", write: "deny" },
});
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
// The merged config leaves a permissive top-level '*' with no bash '*' policy,
// so the bash-fallback footgun warning is expected.
expect(result.issues).toHaveLength(1);
expect(result.issues[0]).toContain("bash");
expect(result.merged.debugLog).toBe(true);
expect(result.merged.permission).toEqual({
"*": "allow",
read: "allow",
write: "deny",
});
});
it("detects legacy global policy and emits migration issue", () => {
writeLegacyGlobalPolicy({
@@ -800,18 +723,6 @@ describe("loadAndMergeConfigs", () => {
expect(result.merged.permission).toBeUndefined();
});
it("detects legacy project policy and emits migration issue", () => {
writeLegacyProjectPolicy({
bash: { "git status": "allow" },
});
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
expect(result.issues).toHaveLength(1);
expect(result.issues[0]).toContain(".pi/agent/pi-permissions.jsonc");
expect(result.issues[0]).toContain(".pi/extensions/pi-permission-system");
// Legacy file has no flat-format permission key — no rules extracted
expect(result.merged.permission).toBeUndefined();
});
it("detects legacy extension runtime config and emits migration issue", () => {
writeLegacyExtensionConfig({
@@ -878,57 +789,15 @@ describe("loadAndMergeConfigs", () => {
expect(result.issues).toEqual([]);
});
describe("includeProjectScope", () => {
it("omits the new project config when includeProjectScope is false", () => {
writeGlobal({
permission: { "*": "ask", bash: "deny" },
});
writeProject({
permission: { bash: "allow" },
});
it("ignores new and legacy project policy files", () => {
writeGlobal({ permission: { "*": "ask", bash: "deny" } });
writeProject({ permission: { bash: "allow" } });
writeLegacyProjectPolicy({ permission: { "*": "allow" } });
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot, {
includeProjectScope: false,
});
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
// The untrusted project's `bash: allow` must not override global `deny`.
expect(result.merged.permission).toEqual({ "*": "ask", bash: "deny" });
expect(result.project).toEqual({});
});
it("omits the legacy project policy when includeProjectScope is false", () => {
writeGlobal({ permission: { "*": "ask" } });
writeLegacyProjectPolicy({ permission: { "*": "allow" } });
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot, {
includeProjectScope: false,
});
expect(result.merged.permission).toEqual({ "*": "ask" });
expect(
result.issues.some((i) => i.includes("pi-permissions.jsonc")),
).toBe(false);
});
it("includes the project config when includeProjectScope is true", () => {
writeGlobal({ permission: { "*": "ask", bash: "deny" } });
writeProject({ permission: { bash: "allow" } });
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot, {
includeProjectScope: true,
});
expect(result.merged.permission).toEqual({ "*": "ask", bash: "allow" });
});
it("includes the project config by default (option omitted)", () => {
writeGlobal({ permission: { "*": "ask", bash: "deny" } });
writeProject({ permission: { bash: "allow" } });
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
expect(result.merged.permission).toEqual({ "*": "ask", bash: "allow" });
});
expect(result.merged.permission).toEqual({ "*": "ask", bash: "deny" });
expect(result.issues).toEqual([]);
});
describe("deprecated tool-preview caps", () => {