mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ResolvedAccessIntent } from "#src/access-intent/access-intent";
|
||||
import { AccessPath } from "#src/access-intent/access-path";
|
||||
import { posixPathFlavor } from "#src/path/path-flavor";
|
||||
import type { ScopedPermissionManager } from "#src/permission-manager";
|
||||
import { PermissionResolver } from "#src/permission-resolver";
|
||||
import type { Ruleset } from "#src/rule";
|
||||
import { SessionApproval } from "#src/session-approval";
|
||||
import { SessionRules } from "#src/session-rules";
|
||||
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
||||
|
||||
function makePermissionManager() {
|
||||
return {
|
||||
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
||||
check: vi
|
||||
.fn<
|
||||
(
|
||||
intent: ResolvedAccessIntent,
|
||||
sessionRules?: Ruleset,
|
||||
) => PermissionCheckResult
|
||||
>()
|
||||
.mockReturnValue({
|
||||
state: "allow",
|
||||
toolName: "read",
|
||||
source: "tool",
|
||||
origin: "builtin",
|
||||
}),
|
||||
getToolPermission: vi
|
||||
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
||||
.mockReturnValue("allow"),
|
||||
getConfigIssues: vi.fn((): string[] => []),
|
||||
};
|
||||
}
|
||||
|
||||
function makeResolver(
|
||||
pm?: ScopedPermissionManager,
|
||||
sessionRules?: Pick<SessionRules, "getRuleset">,
|
||||
) {
|
||||
const permissionManager = pm ?? makePermissionManager();
|
||||
const rules = sessionRules ?? new SessionRules();
|
||||
return {
|
||||
resolver: new PermissionResolver(permissionManager, rules),
|
||||
permissionManager,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// no module-level vi.fn() stubs to reset
|
||||
});
|
||||
|
||||
describe("PermissionResolver", () => {
|
||||
describe("resolve — tool intent", () => {
|
||||
it("forwards a tool intent with the empty session ruleset", () => {
|
||||
const { resolver, permissionManager } = makeResolver();
|
||||
|
||||
resolver.resolve({
|
||||
kind: "tool",
|
||||
surface: "bash",
|
||||
input: { command: "ls" },
|
||||
agentName: "agent-x",
|
||||
});
|
||||
|
||||
expect(permissionManager.check).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: "tool",
|
||||
surface: "bash",
|
||||
input: { command: "ls" },
|
||||
agentName: "agent-x",
|
||||
},
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it("applies a recorded session approval on the next resolve", () => {
|
||||
const pm = makePermissionManager();
|
||||
const sessionRules = new SessionRules();
|
||||
const { resolver } = makeResolver(pm, sessionRules);
|
||||
|
||||
sessionRules.recordSessionApproval(
|
||||
SessionApproval.single("bash", "git *"),
|
||||
);
|
||||
resolver.resolve({
|
||||
kind: "tool",
|
||||
surface: "bash",
|
||||
input: { command: "git status" },
|
||||
});
|
||||
|
||||
const passedRules = vi.mocked(pm.check).mock.calls[0][1];
|
||||
expect(passedRules).toHaveLength(1);
|
||||
expect(passedRules?.[0]).toMatchObject({
|
||||
surface: "bash",
|
||||
pattern: "git *",
|
||||
action: "allow",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the manager's check result", () => {
|
||||
const pm = makePermissionManager();
|
||||
vi.mocked(pm.check).mockReturnValue({
|
||||
state: "deny",
|
||||
toolName: "bash",
|
||||
source: "bash",
|
||||
origin: "global",
|
||||
matchedPattern: "rm *",
|
||||
});
|
||||
const { resolver } = makeResolver(pm);
|
||||
|
||||
const result = resolver.resolve({
|
||||
kind: "tool",
|
||||
surface: "bash",
|
||||
input: { command: "rm -rf /" },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
state: "deny",
|
||||
toolName: "bash",
|
||||
source: "bash",
|
||||
origin: "global",
|
||||
matchedPattern: "rm *",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolve — session ruleset threading", () => {
|
||||
it("applies a recorded session approval on the next call", () => {
|
||||
const pm = makePermissionManager();
|
||||
const sessionRules = new SessionRules();
|
||||
const { resolver } = makeResolver(pm, sessionRules);
|
||||
|
||||
sessionRules.recordSessionApproval(
|
||||
SessionApproval.single("path", "src/*"),
|
||||
);
|
||||
resolver.resolve({
|
||||
kind: "access-path",
|
||||
surface: "path",
|
||||
path: AccessPath.forPath("src/a.ts", {
|
||||
cwd: "/proj",
|
||||
flavor: posixPathFlavor,
|
||||
}),
|
||||
});
|
||||
|
||||
const passedRules = vi.mocked(pm.check).mock.calls[0][1];
|
||||
expect(passedRules).toHaveLength(1);
|
||||
expect(passedRules?.[0]).toMatchObject({
|
||||
surface: "path",
|
||||
pattern: "src/*",
|
||||
action: "allow",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolve — access-path intent", () => {
|
||||
it("unwraps the AccessPath via matchValues() into a path-values intent", () => {
|
||||
const { resolver, permissionManager } = makeResolver();
|
||||
const accessPath = AccessPath.forPath("/tmp/x", {
|
||||
cwd: "/workspace",
|
||||
flavor: posixPathFlavor,
|
||||
});
|
||||
|
||||
resolver.resolve({
|
||||
kind: "access-path",
|
||||
surface: "external_directory",
|
||||
path: accessPath,
|
||||
agentName: "agent-x",
|
||||
});
|
||||
|
||||
expect(permissionManager.check).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: "path-values",
|
||||
surface: "external_directory",
|
||||
values: accessPath.matchValues(),
|
||||
agentName: "agent-x",
|
||||
},
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the manager's check result for an access-path intent", () => {
|
||||
const pm = makePermissionManager();
|
||||
vi.mocked(pm.check).mockReturnValue({
|
||||
state: "deny",
|
||||
toolName: "external_directory",
|
||||
source: "special",
|
||||
origin: "global",
|
||||
matchedPattern: "/tmp/*",
|
||||
});
|
||||
const { resolver } = makeResolver(pm);
|
||||
const accessPath = AccessPath.forPath("/tmp/x", {
|
||||
cwd: "/workspace",
|
||||
flavor: posixPathFlavor,
|
||||
});
|
||||
|
||||
const result = resolver.resolve({
|
||||
kind: "access-path",
|
||||
surface: "external_directory",
|
||||
path: accessPath,
|
||||
});
|
||||
|
||||
expect(result.state).toBe("deny");
|
||||
expect(result.matchedPattern).toBe("/tmp/*");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolve — pre-fixed path-values intent (forwarded-serving producer)", () => {
|
||||
// #597: the forwarded-serving wire hands the resolver a ResolvedAccessIntent
|
||||
// it built directly from the child-fixed matchValues() — never an
|
||||
// AccessPath. The resolver must pass it through to the manager unchanged
|
||||
// (no matchValues() unwrap, since there is no AccessPath to unwrap).
|
||||
it("passes a path-values intent to the manager unchanged, with the composed session ruleset", () => {
|
||||
const pm = makePermissionManager();
|
||||
const sessionRules = new SessionRules();
|
||||
const { resolver } = makeResolver(pm, sessionRules);
|
||||
|
||||
sessionRules.recordSessionApproval(
|
||||
SessionApproval.single("external_directory", "/tmp/*"),
|
||||
);
|
||||
|
||||
resolver.resolve({
|
||||
kind: "path-values",
|
||||
surface: "external_directory",
|
||||
values: ["/tmp/x", "/real/tmp/x"],
|
||||
agentName: "Explore",
|
||||
});
|
||||
|
||||
const [passedIntent, passedRules] = vi.mocked(pm.check).mock.calls[0];
|
||||
expect(passedIntent).toEqual({
|
||||
kind: "path-values",
|
||||
surface: "external_directory",
|
||||
values: ["/tmp/x", "/real/tmp/x"],
|
||||
agentName: "Explore",
|
||||
});
|
||||
expect(passedRules).toHaveLength(1);
|
||||
expect(passedRules?.[0]).toMatchObject({
|
||||
surface: "external_directory",
|
||||
pattern: "/tmp/*",
|
||||
action: "allow",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the manager's check result for a path-values intent", () => {
|
||||
const pm = makePermissionManager();
|
||||
vi.mocked(pm.check).mockReturnValue({
|
||||
state: "allow",
|
||||
toolName: "path",
|
||||
source: "special",
|
||||
origin: "global",
|
||||
matchedPattern: "src/**",
|
||||
});
|
||||
const { resolver } = makeResolver(pm);
|
||||
|
||||
const result = resolver.resolve({
|
||||
kind: "path-values",
|
||||
surface: "path",
|
||||
values: ["/worktree/issue-42/src/foo.ts", "src/foo.ts"],
|
||||
agentName: "Explore",
|
||||
});
|
||||
|
||||
expect(result.state).toBe("allow");
|
||||
expect(result.matchedPattern).toBe("src/**");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkPermission (raw, off-interface)", () => {
|
||||
it("delegates to manager.check as a tool intent without session rules", () => {
|
||||
const { resolver, permissionManager } = makeResolver();
|
||||
|
||||
resolver.checkPermission("bash", { command: "ls" }, "agent-1");
|
||||
|
||||
expect(permissionManager.check).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: "tool",
|
||||
surface: "bash",
|
||||
input: { command: "ls" },
|
||||
agentName: "agent-1",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("passes optional sessionRules as the second arg to check", () => {
|
||||
const { resolver, permissionManager } = makeResolver();
|
||||
const extraRules: Ruleset = [
|
||||
{ surface: "bash", pattern: "*", action: "allow", origin: "session" },
|
||||
];
|
||||
|
||||
resolver.checkPermission(
|
||||
"bash",
|
||||
{ command: "ls" },
|
||||
undefined,
|
||||
extraRules,
|
||||
);
|
||||
|
||||
expect(permissionManager.check).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: "tool",
|
||||
surface: "bash",
|
||||
input: { command: "ls" },
|
||||
agentName: undefined,
|
||||
},
|
||||
extraRules,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getToolPermission", () => {
|
||||
it("delegates to permissionManager.getToolPermission", () => {
|
||||
const pm = makePermissionManager();
|
||||
vi.mocked(pm.getToolPermission).mockReturnValue("deny");
|
||||
const { resolver } = makeResolver(pm);
|
||||
|
||||
const result = resolver.getToolPermission("write", "my-agent");
|
||||
|
||||
expect(pm.getToolPermission).toHaveBeenCalledWith("write", "my-agent");
|
||||
expect(result).toBe("deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getConfigIssues", () => {
|
||||
it("delegates to permissionManager.getConfigIssues", () => {
|
||||
const pm = makePermissionManager();
|
||||
vi.mocked(pm.getConfigIssues).mockReturnValue(["issue-1"]);
|
||||
const { resolver } = makeResolver(pm);
|
||||
|
||||
const result = resolver.getConfigIssues("agent-1");
|
||||
|
||||
expect(pm.getConfigIssues).toHaveBeenCalledWith("agent-1");
|
||||
expect(result).toEqual(["issue-1"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user