mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
refactor(permission-system): simplify policy and autoreview flow
This commit is contained in:
@@ -136,76 +136,6 @@ describe("renderPolicyDenial", () => {
|
||||
).toBe("[pi-permission-system] Denied by policy: 'bash'.");
|
||||
});
|
||||
|
||||
it("names the escaped boundary for a tool external-directory deny", () => {
|
||||
expect(
|
||||
renderPolicyDenial(
|
||||
payload(
|
||||
"external_directory",
|
||||
{
|
||||
surface: "external_directory",
|
||||
toolName: "write",
|
||||
value: "/etc/hosts",
|
||||
matchedPattern: "*",
|
||||
},
|
||||
[{ label: "working directory", text: "/repo", detail: null }],
|
||||
),
|
||||
null,
|
||||
),
|
||||
).toBe(
|
||||
"[pi-permission-system] Denied by policy: 'external_directory' for tool 'write' for path '/etc/hosts' (rule '*'): outside working directory '/repo'.",
|
||||
);
|
||||
});
|
||||
|
||||
it("names the canonical target when a path resolves elsewhere", () => {
|
||||
expect(
|
||||
renderPolicyDenial(
|
||||
payload(
|
||||
"external_directory",
|
||||
{
|
||||
surface: "external_directory",
|
||||
toolName: "read",
|
||||
value: "link",
|
||||
matchedPattern: "*",
|
||||
},
|
||||
[
|
||||
{ label: "resolves to", text: "/etc/shadow", detail: null },
|
||||
{ label: "working directory", text: "/repo", detail: null },
|
||||
],
|
||||
),
|
||||
null,
|
||||
),
|
||||
).toBe(
|
||||
"[pi-permission-system] Denied by policy: 'external_directory' for tool 'read' for path 'link' (resolves to '/etc/shadow') (rule '*'): outside working directory '/repo'.",
|
||||
);
|
||||
});
|
||||
|
||||
it("names every escaping path but not the command for a bash external-directory deny", () => {
|
||||
expect(
|
||||
renderPolicyDenial(
|
||||
payload(
|
||||
"bash_external_directory",
|
||||
{
|
||||
surface: "external_directory",
|
||||
toolName: "bash",
|
||||
value: "diff /etc/hosts ~/.ssh/config",
|
||||
matchedPattern: "*",
|
||||
},
|
||||
[
|
||||
{ label: "working directory", text: "/repo", detail: null },
|
||||
{ label: "external path", text: "/etc/hosts", detail: null },
|
||||
{
|
||||
label: "external path",
|
||||
text: "~/.ssh/config",
|
||||
detail: "/home/me/.ssh/config",
|
||||
},
|
||||
],
|
||||
),
|
||||
null,
|
||||
),
|
||||
).toBe(
|
||||
"[pi-permission-system] Denied by policy: 'external_directory' for tool 'bash' for paths '/etc/hosts', '~/.ssh/config' (resolves to '/home/me/.ssh/config') (rule '*'): outside working directory '/repo'.",
|
||||
);
|
||||
});
|
||||
|
||||
it("names the MCP target", () => {
|
||||
expect(
|
||||
@@ -370,25 +300,6 @@ describe("renderUnavailableDenial", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("omits the escaped boundary, which no retry shape would change", () => {
|
||||
expect(
|
||||
renderUnavailableDenial(
|
||||
payload(
|
||||
"external_directory",
|
||||
{
|
||||
surface: "external_directory",
|
||||
toolName: "write",
|
||||
value: "/etc/hosts",
|
||||
matchedPattern: "*",
|
||||
},
|
||||
[{ label: "working directory", text: "/repo", detail: null }],
|
||||
),
|
||||
null,
|
||||
),
|
||||
).toBe(
|
||||
"[pi-permission-system] This 'external_directory' call for tool 'write' for path '/etc/hosts' (rule '*') requires approval, but no interactive UI is available.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the flagged-element field cap", () => {
|
||||
|
||||
@@ -48,24 +48,6 @@ describe("renderPromptDialog", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("names the gate surface when it differs from the tool", () => {
|
||||
expect(
|
||||
render({
|
||||
kind: "external_directory",
|
||||
request: requestFacts({
|
||||
surface: "external_directory",
|
||||
toolName: "write",
|
||||
value: "/etc/hosts",
|
||||
matchedPattern: "*",
|
||||
}),
|
||||
}),
|
||||
).toEqual([
|
||||
"tool : write",
|
||||
"surface : external_directory",
|
||||
"rule : *",
|
||||
"path : /etc/hosts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits an agent line for an unnamed local requester, and a surface the value label already names", () => {
|
||||
expect(
|
||||
@@ -214,36 +196,6 @@ describe("renderPromptDialog", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps an entry's detail on the entry's own line", () => {
|
||||
expect(
|
||||
render({
|
||||
kind: "bash_external_directory",
|
||||
request: requestFacts({
|
||||
surface: "external_directory",
|
||||
toolName: "bash",
|
||||
value: "cat /etc/hosts /tmp/x",
|
||||
matchedPattern: "*",
|
||||
}),
|
||||
evidence: [
|
||||
{ label: "working directory", text: "/repo", detail: null },
|
||||
{
|
||||
label: "external path",
|
||||
text: "/etc/hosts",
|
||||
detail: "/private/etc/hosts",
|
||||
},
|
||||
{ label: "external path", text: "/tmp/x", detail: null },
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
"tool : bash",
|
||||
"surface : external_directory",
|
||||
"rule : *",
|
||||
"command : cat /etc/hosts /tmp/x",
|
||||
"working directory : /repo",
|
||||
"external path : /etc/hosts → /private/etc/hosts",
|
||||
"external path : /tmp/x",
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders a skill read's path", () => {
|
||||
expect(
|
||||
@@ -558,35 +510,6 @@ describe("renderPromptDialog", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("paints each escaping path rather than the command that referenced them", () => {
|
||||
expect(
|
||||
renderPromptDialog(
|
||||
makePromptPayload({
|
||||
kind: "bash_external_directory",
|
||||
request: {
|
||||
...makePromptPayload().request,
|
||||
surface: "external_directory",
|
||||
toolName: "bash",
|
||||
value: "cat /etc/hosts /etc/hostsbackup",
|
||||
matchedPattern: "*",
|
||||
},
|
||||
evidence: [
|
||||
{ label: "working directory", text: "/repo", detail: null },
|
||||
{ label: "external path", text: "/etc/hosts", detail: null },
|
||||
],
|
||||
}),
|
||||
completeViewBudget(200),
|
||||
mark,
|
||||
).lines,
|
||||
).toEqual([
|
||||
"tool : bash",
|
||||
"surface : external_directory",
|
||||
"rule : *",
|
||||
"command : cat [/etc/hosts] /etc/hostsbackup",
|
||||
"working directory : /repo",
|
||||
"external path : [/etc/hosts]",
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves the text untouched when no paint is supplied", () => {
|
||||
expect(
|
||||
|
||||
@@ -26,10 +26,6 @@ function payloadOf(
|
||||
};
|
||||
}
|
||||
|
||||
/** An `external path` evidence entry, as the bash external-directory gate emits it. */
|
||||
function externalPath(path: string, resolved: string | null): PromptEvidence {
|
||||
return { label: "external path", text: path, detail: resolved };
|
||||
}
|
||||
|
||||
describe("flaggedElements", () => {
|
||||
it("flags the decision-relevant value for a single-value ask", () => {
|
||||
@@ -44,17 +40,6 @@ describe("flaggedElements", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags the escaping paths rather than the command for a bash external-directory ask", () => {
|
||||
expect(
|
||||
flaggedElements(
|
||||
payloadOf("bash_external_directory", "diff /etc/hosts ~/.ssh/config", [
|
||||
{ label: "working directory", text: "/repo", detail: null },
|
||||
externalPath("/etc/hosts", null),
|
||||
externalPath("~/.ssh/config", "/home/me/.ssh/config"),
|
||||
]),
|
||||
),
|
||||
).toEqual(["/etc/hosts", "~/.ssh/config"]);
|
||||
});
|
||||
|
||||
it("flags nothing when the value is empty", () => {
|
||||
expect(flaggedElements(payloadOf("tool", ""))).toEqual([]);
|
||||
@@ -64,11 +49,9 @@ describe("flaggedElements", () => {
|
||||
describe("valueLabel", () => {
|
||||
it.each([
|
||||
["bash", "command"],
|
||||
["bash_external_directory", "command"],
|
||||
["mcp", "target"],
|
||||
["tool", "tool"],
|
||||
["path", "path"],
|
||||
["external_directory", "path"],
|
||||
["skill", "skill"],
|
||||
["skill_read", "skill"],
|
||||
] as const)("labels a %s ask's value %s", (kind, label) => {
|
||||
@@ -88,11 +71,6 @@ describe("valueLabel", () => {
|
||||
});
|
||||
|
||||
describe("flaggedElementLabel", () => {
|
||||
it("labels the escaping paths a bash external-directory ask flags", () => {
|
||||
expect(
|
||||
flaggedElementLabel(payloadOf("bash_external_directory", "cmd")),
|
||||
).toBe("path");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"bash",
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
buildBashExternalDirectoryAskPayload,
|
||||
buildExternalDirectoryAskPayload,
|
||||
buildPathAskPayload,
|
||||
} from "#src/presentation/path-ask-payload";
|
||||
import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
|
||||
|
||||
describe("buildPathAskPayload", () => {
|
||||
test("carries the typed path as the decision value and the rule that fired", () => {
|
||||
@@ -38,89 +34,3 @@ describe("buildPathAskPayload", () => {
|
||||
).toEqual({ agentName: null, forwarded: false, sessionId: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildExternalDirectoryAskPayload", () => {
|
||||
test("carries the typed path, the boundary, and the requester", () => {
|
||||
const payload = buildExternalDirectoryAskPayload({
|
||||
toolName: "write",
|
||||
pathValue: "/tmp/out.txt",
|
||||
cwd: "/projects/my-app",
|
||||
agentName: "my-agent",
|
||||
});
|
||||
|
||||
expect(payload.kind).toBe("external_directory");
|
||||
expect(payload.request.toolName).toBe("write");
|
||||
expect(payload.request.value).toBe("/tmp/out.txt");
|
||||
expect(payload.request.requester.agentName).toBe("my-agent");
|
||||
expect(payload.evidence).toEqual([
|
||||
{ label: "working directory", text: "/projects/my-app", detail: null },
|
||||
]);
|
||||
});
|
||||
|
||||
test("discloses the resolved path as its own entry when it differs", () => {
|
||||
expect(
|
||||
buildExternalDirectoryAskPayload({
|
||||
toolName: "read",
|
||||
pathValue: "demo-symlink-passwd",
|
||||
resolvedPath: "/etc/passwd",
|
||||
cwd: "/projects/my-app",
|
||||
agentName: null,
|
||||
}).evidence,
|
||||
).toEqual([
|
||||
{ label: "resolves to", text: "/etc/passwd", detail: null },
|
||||
{ label: "working directory", text: "/projects/my-app", detail: null },
|
||||
]);
|
||||
});
|
||||
|
||||
test("omits the disclosure when resolvedPath is undefined", () => {
|
||||
const payload = buildExternalDirectoryAskPayload({
|
||||
toolName: "read",
|
||||
pathValue: "/etc/passwd",
|
||||
cwd: "/projects/my-app",
|
||||
agentName: null,
|
||||
});
|
||||
|
||||
expect(payload.evidence).toEqual([
|
||||
{ label: "working directory", text: "/projects/my-app", detail: null },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildBashExternalDirectoryAskPayload", () => {
|
||||
test("makes the command the value and the paths it reached evidence", () => {
|
||||
const payload = buildBashExternalDirectoryAskPayload({
|
||||
command: "cat /etc/passwd",
|
||||
externalPaths: [{ path: "/etc/passwd" }],
|
||||
cwd: "/projects/my-app",
|
||||
agentName: "my-agent",
|
||||
toolName: "bash",
|
||||
});
|
||||
|
||||
expect(payload.kind).toBe("bash_external_directory");
|
||||
expect(payload.request.value).toBe("cat /etc/passwd");
|
||||
expect(payload.request.requester.agentName).toBe("my-agent");
|
||||
expect(payload.evidence).toEqual([
|
||||
{ label: "working directory", text: "/projects/my-app", detail: null },
|
||||
{ label: "external path", text: "/etc/passwd", detail: null },
|
||||
]);
|
||||
});
|
||||
|
||||
test("binds each path's canonical alias to its own entry", () => {
|
||||
const payload = buildBashExternalDirectoryAskPayload({
|
||||
command: "cat a b",
|
||||
externalPaths: [
|
||||
{ path: "/a", resolvedPath: "/private/a" },
|
||||
{ path: "/b" },
|
||||
],
|
||||
cwd: "/repo",
|
||||
agentName: null,
|
||||
toolName: "bash",
|
||||
});
|
||||
|
||||
expect(payload.evidence).toEqual([
|
||||
{ label: "working directory", text: "/repo", detail: null },
|
||||
{ label: "external path", text: "/a", detail: "/private/a" },
|
||||
{ label: "external path", text: "/b", detail: null },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user