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,269 @@
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
|
||||
import type { AccessIntent } from "#src/access-intent/access-intent";
|
||||
import type { AuthorizerRegistrar } from "#src/authority/authorizer-registry";
|
||||
import { posixPathFlavor } from "#src/path/path-flavor";
|
||||
import { PathNormalizer } from "#src/path-normalizer";
|
||||
import { LocalPermissionsService } from "#src/permissions-service";
|
||||
import type { ToolAccessExtractorRegistrar } from "#src/tool-access-extractor-registry";
|
||||
import type {
|
||||
ToolInputFormatter,
|
||||
ToolInputFormatterRegistrar,
|
||||
} from "#src/tool-input-formatter-registry";
|
||||
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
||||
|
||||
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
||||
|
||||
// Mock node:fs so realpathSync (the canonical alias) is controllable.
|
||||
const realpathSync = vi.hoisted(() =>
|
||||
vi.fn<(path: string) => string>((p) => p),
|
||||
);
|
||||
vi.mock("node:fs", () => ({
|
||||
realpathSync,
|
||||
default: { realpathSync },
|
||||
}));
|
||||
|
||||
// Mock the advisory bash resolver so the service test asserts delegation; the
|
||||
// decomposition behavior itself is covered in bash-advisory-check.test.ts.
|
||||
const resolveBashAdvisoryCheck = vi.hoisted(() =>
|
||||
vi.fn<
|
||||
(
|
||||
command: string,
|
||||
agentName: string | undefined,
|
||||
resolver: unknown,
|
||||
) => PermissionCheckResult
|
||||
>(),
|
||||
);
|
||||
vi.mock("#src/bash-advisory-check", () => ({ resolveBashAdvisoryCheck }));
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface FakeResolver {
|
||||
resolve: Mock<(intent: AccessIntent) => PermissionCheckResult>;
|
||||
getToolPermission: Mock<
|
||||
(toolName: string, agentName?: string) => PermissionState
|
||||
>;
|
||||
}
|
||||
|
||||
function makeResolver(): FakeResolver {
|
||||
return {
|
||||
resolve: vi
|
||||
.fn<(intent: AccessIntent) => PermissionCheckResult>()
|
||||
.mockReturnValue(makeCheckResult()),
|
||||
getToolPermission: vi
|
||||
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
||||
.mockReturnValue("ask"),
|
||||
};
|
||||
}
|
||||
|
||||
function makeFormatterRegistry(): ToolInputFormatterRegistrar {
|
||||
return {
|
||||
register: vi
|
||||
.fn<ToolInputFormatterRegistrar["register"]>()
|
||||
.mockReturnValue(vi.fn()),
|
||||
};
|
||||
}
|
||||
|
||||
function makeAccessExtractorRegistry(): ToolAccessExtractorRegistrar {
|
||||
return {
|
||||
register: vi
|
||||
.fn<ToolAccessExtractorRegistrar["register"]>()
|
||||
.mockReturnValue(vi.fn()),
|
||||
};
|
||||
}
|
||||
|
||||
function makeAuthorizerRegistry(): AuthorizerRegistrar {
|
||||
return {
|
||||
register: vi.fn<AuthorizerRegistrar["register"]>().mockReturnValue(vi.fn()),
|
||||
};
|
||||
}
|
||||
|
||||
function makeService(overrides?: {
|
||||
resolver?: FakeResolver;
|
||||
formatterRegistry?: ToolInputFormatterRegistrar;
|
||||
accessExtractorRegistry?: ToolAccessExtractorRegistrar;
|
||||
authorizerRegistry?: AuthorizerRegistrar;
|
||||
}) {
|
||||
const resolver = overrides?.resolver ?? makeResolver();
|
||||
// The published service always answers against the parent session's cwd.
|
||||
const session = { getPathNormalizer: () => normalizer };
|
||||
const formatterRegistry =
|
||||
overrides?.formatterRegistry ?? makeFormatterRegistry();
|
||||
const accessExtractorRegistry =
|
||||
overrides?.accessExtractorRegistry ?? makeAccessExtractorRegistry();
|
||||
const authorizerRegistry =
|
||||
overrides?.authorizerRegistry ?? makeAuthorizerRegistry();
|
||||
const service = new LocalPermissionsService(
|
||||
resolver,
|
||||
session,
|
||||
formatterRegistry,
|
||||
accessExtractorRegistry,
|
||||
authorizerRegistry,
|
||||
);
|
||||
return {
|
||||
service,
|
||||
resolver,
|
||||
formatterRegistry,
|
||||
accessExtractorRegistry,
|
||||
authorizerRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
const normalizer = new PathNormalizer(posixPathFlavor, "/test/project");
|
||||
|
||||
// ── tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
beforeEach(() => {
|
||||
realpathSync.mockReset();
|
||||
realpathSync.mockImplementation((p: string) => p);
|
||||
resolveBashAdvisoryCheck.mockReset();
|
||||
resolveBashAdvisoryCheck.mockReturnValue(
|
||||
makeCheckResult({ toolName: "bash" }),
|
||||
);
|
||||
});
|
||||
|
||||
describe("checkPermission", () => {
|
||||
it("resolves a non-path surface through a tool intent", () => {
|
||||
const { service, resolver } = makeService();
|
||||
service.checkPermission("skill", "my-skill", "my-agent");
|
||||
expect(resolver.resolve).toHaveBeenCalledWith({
|
||||
kind: "tool",
|
||||
surface: "skill",
|
||||
input: { name: "my-skill" },
|
||||
agentName: "my-agent",
|
||||
});
|
||||
});
|
||||
|
||||
it("routes a bash query through the advisory decomposition resolver", () => {
|
||||
const { service, resolver } = makeService();
|
||||
const expected = makeCheckResult({ state: "deny", toolName: "bash" });
|
||||
resolveBashAdvisoryCheck.mockReturnValue(expected);
|
||||
const result = service.checkPermission(
|
||||
"bash",
|
||||
"cd /repo && npm install x",
|
||||
"my-agent",
|
||||
);
|
||||
expect(resolveBashAdvisoryCheck).toHaveBeenCalledWith(
|
||||
"cd /repo && npm install x",
|
||||
"my-agent",
|
||||
resolver,
|
||||
);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("passes an empty string to the advisory resolver for a value-less bash query", () => {
|
||||
const { service, resolver } = makeService();
|
||||
service.checkPermission("bash");
|
||||
expect(resolveBashAdvisoryCheck).toHaveBeenCalledWith(
|
||||
"",
|
||||
undefined,
|
||||
resolver,
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves an external_directory path query through an access-path intent matching the canonical alias", () => {
|
||||
realpathSync.mockImplementation((p: string) =>
|
||||
p === "/test/project/link" ? "/test/project/real" : p,
|
||||
);
|
||||
const { service, resolver } = makeService();
|
||||
service.checkPermission("external_directory", "link");
|
||||
expect(resolver.resolve).toHaveBeenCalledTimes(1);
|
||||
const intent = resolver.resolve.mock.calls[0][0];
|
||||
expect(intent.kind).toBe("access-path");
|
||||
if (intent.kind === "access-path") {
|
||||
expect(intent.surface).toBe("external_directory");
|
||||
expect(intent.path.matchValues()).toContain("/test/project/real");
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves a path-bearing tool query (read) through an access-path intent", () => {
|
||||
const { service, resolver } = makeService();
|
||||
service.checkPermission("read", "/test/project/.env");
|
||||
const intent = resolver.resolve.mock.calls[0][0];
|
||||
expect(intent.kind).toBe("access-path");
|
||||
if (intent.kind === "access-path") {
|
||||
expect(intent.surface).toBe("read");
|
||||
expect(intent.path.value()).toBe("/test/project/.env");
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to a tool intent for a value-less path query", () => {
|
||||
const { service, resolver } = makeService();
|
||||
service.checkPermission("path");
|
||||
const intent = resolver.resolve.mock.calls[0][0];
|
||||
expect(intent.kind).toBe("tool");
|
||||
});
|
||||
|
||||
it("returns the result from resolver.resolve", () => {
|
||||
const expected = makeCheckResult({ state: "deny", toolName: "skill" });
|
||||
const resolver = makeResolver();
|
||||
resolver.resolve.mockReturnValue(expected);
|
||||
const { service } = makeService({ resolver });
|
||||
const result = service.checkPermission("skill", "my-skill");
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getToolPermission", () => {
|
||||
it("delegates to resolver.getToolPermission", () => {
|
||||
const resolver = makeResolver();
|
||||
resolver.getToolPermission.mockReturnValue("deny");
|
||||
const { service } = makeService({ resolver });
|
||||
const result = service.getToolPermission("write", "my-agent");
|
||||
expect(resolver.getToolPermission).toHaveBeenCalledWith(
|
||||
"write",
|
||||
"my-agent",
|
||||
);
|
||||
expect(result).toBe("deny");
|
||||
});
|
||||
|
||||
it("omits agentName when not provided", () => {
|
||||
const { service, resolver } = makeService();
|
||||
service.getToolPermission("read");
|
||||
expect(resolver.getToolPermission).toHaveBeenCalledWith("read", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerToolInputFormatter", () => {
|
||||
it("delegates to formatterRegistry.register and returns the unsubscribe function", () => {
|
||||
const unsub = vi.fn();
|
||||
const { service, formatterRegistry } = makeService();
|
||||
vi.mocked(formatterRegistry.register).mockReturnValue(unsub);
|
||||
const formatter: ToolInputFormatter = vi.fn();
|
||||
const result = service.registerToolInputFormatter("my-tool", formatter);
|
||||
expect(formatterRegistry.register).toHaveBeenCalledWith(
|
||||
"my-tool",
|
||||
formatter,
|
||||
);
|
||||
expect(result).toBe(unsub);
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerToolAccessExtractor", () => {
|
||||
it("delegates to accessExtractorRegistry.register and returns the unsubscribe function", () => {
|
||||
const unsub = vi.fn();
|
||||
const { service, accessExtractorRegistry } = makeService();
|
||||
vi.mocked(accessExtractorRegistry.register).mockReturnValue(unsub);
|
||||
const extractor = vi.fn();
|
||||
const result = service.registerToolAccessExtractor("ffgrep", extractor);
|
||||
expect(accessExtractorRegistry.register).toHaveBeenCalledWith(
|
||||
"ffgrep",
|
||||
extractor,
|
||||
);
|
||||
expect(result).toBe(unsub);
|
||||
});
|
||||
});
|
||||
|
||||
describe("registerAuthorizer", () => {
|
||||
it("delegates to authorizerRegistry.register and returns the unsubscribe function", () => {
|
||||
const unsub = vi.fn();
|
||||
const { service, authorizerRegistry } = makeService();
|
||||
vi.mocked(authorizerRegistry.register).mockReturnValue(unsub);
|
||||
const authorize = vi.fn();
|
||||
const result = service.registerAuthorizer("model-judge", authorize);
|
||||
expect(authorizerRegistry.register).toHaveBeenCalledWith(
|
||||
"model-judge",
|
||||
authorize,
|
||||
);
|
||||
expect(result).toBe(unsub);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user