feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
@@ -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"]);
});
});
});