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,304 @@
import { describe, expect, it } from "vitest";
import type {
GateBypass,
GateDescriptor,
} from "#src/handlers/gates/descriptor";
import { isGateBypass, isGateDescriptor } from "#src/handlers/gates/descriptor";
import { describeExternalDirectoryGate } from "#src/handlers/gates/external-directory";
import type { ToolCallContext } from "#src/handlers/gates/types";
import { pathFlavorForPlatform } from "#src/path/path-flavor";
import { PathNormalizer } from "#src/path-normalizer";
import type { ScopedPermissionResolver } from "#src/permission-resolver";
import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
import { makeResolver } from "#test/helpers/gate-fixtures";
import { makeCheckResult } from "#test/helpers/handler-fixtures";
// ── helpers ──────────────────────────────────────────────────────────────
function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
return {
toolName: "read",
agentName: null,
input: { path: "/outside/project/file.ts" },
toolCallId: "tc-1",
cwd: "/test/project",
...overrides,
};
}
// Default resolver for descriptor-shape tests that do not assert the resolved
// state: returns `ask` for the external_directory surface so a descriptor is
// produced. Tests that assert the typed+resolved matching pass an explicit
// resolver to `describeExternalDirectoryGate` directly.
function gateUnderTest(
tcc: ToolCallContext,
infraDirs: string[],
extractors?: ToolAccessExtractorLookup,
resolver: ScopedPermissionResolver = makeResolver(
makeCheckResult({ state: "ask", toolName: "external_directory" }),
),
) {
return describeExternalDirectoryGate(
tcc,
infraDirs,
resolver,
new PathNormalizer(pathFlavorForPlatform(process.platform), tcc.cwd),
extractors,
);
}
// ── tests ────────────────────────────────────────────────────────────────
describe("describeExternalDirectoryGate", () => {
it("returns null when tool is not path-bearing", () => {
const result = gateUnderTest(
makeTcc({ toolName: "bash", input: { command: "ls" } }),
["/test/agent"],
);
expect(result).toBeNull();
});
it("returns null when path is inside CWD", () => {
const result = gateUnderTest(
makeTcc({ input: { path: "/test/project/src/index.ts" } }),
["/test/agent"],
);
expect(result).toBeNull();
});
// ── Pi infrastructure read bypass ─────────────────────────────────────
it("returns GateBypass for read targeting an infra dir", () => {
const result = gateUnderTest(
makeTcc({
toolName: "read",
input: { path: "/test/agent/git/some-package/SKILL.md" },
}),
["/test/agent", "/test/agent/git"],
);
expect(result).not.toBeNull();
expect(isGateBypass(result)).toBe(true);
const bypass = result as GateBypass;
expect(bypass.action).toBe("allow");
expect(bypass.decision).toMatchObject({
resolution: "infrastructure_auto_allowed",
result: "allow",
});
expect(bypass.log).toMatchObject({
event: "permission_request.infrastructure_auto_allowed",
});
// Containment allowed this, not a rule the operator wrote.
expect(bypass.decidedBy).toEqual({ kind: "infrastructure_read" });
});
it("returns GateBypass respecting custom infraDirs", () => {
const result = gateUnderTest(
makeTcc({
toolName: "read",
input: { path: "/custom/infra/SKILL.md" },
}),
["/custom/infra"],
);
expect(isGateBypass(result)).toBe(true);
});
it("does NOT bypass for write tools targeting infra dirs", () => {
const result = gateUnderTest(
makeTcc({
toolName: "write",
input: { path: "/test/agent/git/some-file.ts", content: "x" },
}),
["/test/agent", "/test/agent/git"],
);
// Should be a GateDescriptor (needs permission check), not a bypass
expect(result).not.toBeNull();
expect(isGateDescriptor(result)).toBe(true);
});
// ── GateDescriptor for external paths ─────────────────────────────────
it("returns GateDescriptor with surface 'external_directory'", () => {
const result = gateUnderTest(makeTcc(), ["/test/agent"]);
expect(isGateDescriptor(result)).toBe(true);
const desc = result as GateDescriptor;
expect(desc.surface).toBe("external_directory");
});
it("decision value is the external path", () => {
const result = gateUnderTest(
makeTcc({ input: { path: "/outside/project/file.ts" } }),
["/test/agent"],
) as GateDescriptor;
expect(result.decision.value).toBe("/outside/project/file.ts");
expect(result.decision.surface).toBe("external_directory");
});
it("carries the child-fixed access facts on promptDetails (external_directory surface)", () => {
const path = "/outside/project/file.ts";
const result = gateUnderTest(makeTcc({ input: { path } }), [
"/test/agent",
]) as GateDescriptor;
const accessPath = new PathNormalizer(
pathFlavorForPlatform(process.platform),
"/test/project",
).forPath(path);
expect(result.promptDetails.accessIntent).toEqual({
surface: "external_directory",
matchValues: accessPath.matchValues(),
boundaryValue: accessPath.boundaryValue(),
});
});
it("emits an external_directory payload carrying the escaped boundary", () => {
const result = gateUnderTest(
makeTcc({ input: { path: "/outside/project/file.ts" } }),
["/test/agent"],
) as GateDescriptor;
expect(result.payload.kind).toBe("external_directory");
expect(result.payload.request.value).toBe("/outside/project/file.ts");
expect(result.payload.evidence).toContainEqual({
label: "working directory",
text: "/test/project",
detail: null,
});
});
it("carries a precomputed preCheck and an empty input (matching is done by the gate)", () => {
const result = gateUnderTest(
makeTcc({ input: { path: "/outside/project/file.ts" } }),
["/test/agent"],
) as GateDescriptor;
expect(result.input).toEqual({});
expect(result.preCheck).toBeDefined();
expect(result.preCheck?.state).toBe("ask");
});
it("resolves the typed and symlink-resolved aliases on the external_directory surface (#418)", () => {
const resolver = makeResolver(
makeCheckResult({ state: "ask", toolName: "external_directory" }),
);
gateUnderTest(
makeTcc({ input: { path: "/outside/project/file.ts" } }),
["/test/agent"],
undefined,
resolver,
);
expect(resolver.resolve).toHaveBeenCalledWith(
expect.objectContaining({
kind: "access-path",
surface: "external_directory",
agentName: undefined,
}),
);
const intent = resolver.resolve.mock.calls[0][0];
expect(intent.kind).toBe("access-path");
if (intent.kind === "access-path") {
expect(intent.path.matchValues()).toEqual(["/outside/project/file.ts"]);
}
});
it("sessionApproval uses deriveApprovalPattern", () => {
const result = gateUnderTest(
makeTcc({ input: { path: "/outside/project/file.ts" } }),
["/test/agent"],
) as GateDescriptor;
expect(result.sessionApproval).toBeDefined();
expect(result.sessionApproval?.surface).toBe("external_directory");
expect(result.sessionApproval?.representativePattern).toBeDefined();
});
it("payload contains the external path and the boundary it escaped", () => {
const result = gateUnderTest(
makeTcc({ input: { path: "/outside/project/file.ts" } }),
["/test/agent"],
) as GateDescriptor;
expect(result.payload.kind).toBe("external_directory");
expect(result.payload.request.toolName).toBe("read");
expect(result.payload.request.value).toBe("/outside/project/file.ts");
expect(result.payload.evidence).toContainEqual({
label: "working directory",
text: "/test/project",
detail: null,
});
});
it("promptDetails includes path and tool_call source", () => {
const result = gateUnderTest(
makeTcc({ toolName: "read", agentName: "agent-1", toolCallId: "tc-5" }),
["/test/agent"],
) as GateDescriptor;
expect(result.promptDetails).toMatchObject({
source: "tool_call",
agentName: "agent-1",
toolCallId: "tc-5",
toolName: "read",
path: "/outside/project/file.ts",
});
});
it("logContext includes the path, and no prompt wording", () => {
const result = gateUnderTest(makeTcc(), ["/test/agent"]) as GateDescriptor;
expect(result.logContext).toMatchObject({
source: "tool_call",
path: "/outside/project/file.ts",
});
// The payload's request facts are stamped by the runner, not the gate.
expect(result.logContext).not.toHaveProperty("message");
});
});
// Extension and MCP tools are now external-directory gated (#352) ───────────
describe("describeExternalDirectoryGate — extension and MCP tools (#352)", () => {
it("gates an extension tool with an external input.path", () => {
const result = gateUnderTest(
makeTcc({
toolName: "my-ext",
input: { path: "/outside/project/file.ts" },
}),
["/test/agent"],
);
expect(isGateDescriptor(result)).toBe(true);
expect((result as GateDescriptor).surface).toBe("external_directory");
});
it("gates an MCP tool with an external arguments.path", () => {
const result = gateUnderTest(
makeTcc({
toolName: "mcp",
input: { arguments: { path: "/outside/project/file.ts" } },
}),
["/test/agent"],
);
expect(isGateDescriptor(result)).toBe(true);
});
it("uses a registered extractor's external path for a custom-shaped tool", () => {
const extractors = {
get: (name: string) =>
name === "ffgrep"
? (input: Record<string, unknown>) =>
typeof input.target === "string" ? input.target : undefined
: undefined,
};
const result = gateUnderTest(
makeTcc({ toolName: "ffgrep", input: { target: "/outside/project/x" } }),
["/test/agent"],
extractors,
);
expect(isGateDescriptor(result)).toBe(true);
});
it("returns null for an extension tool whose path is inside cwd", () => {
const result = gateUnderTest(
makeTcc({
toolName: "my-ext",
input: { path: "/test/project/src/x.ts" },
}),
["/test/agent"],
);
expect(result).toBeNull();
});
});