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,168 @@
|
||||
/**
|
||||
* Acceptance test for issue #418.
|
||||
*
|
||||
* Reproduces the reported bug with a real symlink (no `realpathSync` mock):
|
||||
* an `external_directory` allow configured for the path as the user types it
|
||||
* (`<link>/*`) must allow access even though the OS resolves `<link>` to a
|
||||
* different canonical directory. Exercised end-to-end through the real
|
||||
* `PermissionManager` + `PermissionResolver` for both a path-bearing tool and
|
||||
* a bash command, and for an allow keyed on the symlink-resolved form too.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { BashProgram } from "#src/access-intent/bash/program";
|
||||
import { describeBashExternalDirectoryGate } from "#src/handlers/gates/bash-external-directory";
|
||||
import {
|
||||
type GateDescriptor,
|
||||
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 { PermissionResolver } from "#src/permission-resolver";
|
||||
import { SessionRules } from "#src/session-rules";
|
||||
import type { ScopeConfig } from "#src/types";
|
||||
|
||||
import { createManager } from "#test/helpers/manager-harness";
|
||||
|
||||
// ── real symlink fixture ─────────────────────────────────────────────────────
|
||||
|
||||
let realDir: string;
|
||||
let linkDir: string;
|
||||
let cwd: string;
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
function mkTemp(prefix: string): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), prefix));
|
||||
tempRoots.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
realDir = mkTemp("ext-real-");
|
||||
const linkParent = mkTemp("ext-link-");
|
||||
linkDir = join(linkParent, "link");
|
||||
symlinkSync(realDir, linkDir);
|
||||
cwd = mkTemp("ext-cwd-");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (tempRoots.length > 0) {
|
||||
const dir = tempRoots.pop();
|
||||
if (dir) rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeResolver(config: ScopeConfig) {
|
||||
const { manager, cleanup } = createManager(config);
|
||||
manager.configureForCwd(cwd);
|
||||
const resolver = new PermissionResolver(manager, new SessionRules());
|
||||
return { resolver, cleanup };
|
||||
}
|
||||
|
||||
function readTcc(): ToolCallContext {
|
||||
return {
|
||||
toolName: "read",
|
||||
agentName: null,
|
||||
input: { path: join(linkDir, "file.ts") },
|
||||
toolCallId: "tc-1",
|
||||
cwd,
|
||||
};
|
||||
}
|
||||
|
||||
// ── tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("external_directory symlink acceptance (#418)", () => {
|
||||
it("allows a path-bearing tool when the allow is keyed on the typed (symlinked) path", () => {
|
||||
const { resolver, cleanup } = makeResolver({
|
||||
permission: {
|
||||
external_directory: { "*": "ask", [`${linkDir}/*`]: "allow" },
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = describeExternalDirectoryGate(
|
||||
readTcc(),
|
||||
[],
|
||||
resolver,
|
||||
new PathNormalizer(pathFlavorForPlatform(process.platform), cwd),
|
||||
);
|
||||
expect(isGateDescriptor(result)).toBe(true);
|
||||
expect((result as GateDescriptor).preCheck?.state).toBe("allow");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("allows a path-bearing tool when the allow is keyed on the resolved path", () => {
|
||||
// Key the allow on the fully symlink-resolved directory (on macOS the
|
||||
// tmpdir root itself is a symlink, e.g. /var -> /private/var).
|
||||
const resolved = realpathSync(realDir);
|
||||
const { resolver, cleanup } = makeResolver({
|
||||
permission: {
|
||||
external_directory: { "*": "ask", [`${resolved}/*`]: "allow" },
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = describeExternalDirectoryGate(
|
||||
readTcc(),
|
||||
[],
|
||||
resolver,
|
||||
new PathNormalizer(pathFlavorForPlatform(process.platform), cwd),
|
||||
);
|
||||
expect(isGateDescriptor(result)).toBe(true);
|
||||
expect((result as GateDescriptor).preCheck?.state).toBe("allow");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("still prompts (ask) when no external_directory allow matches", () => {
|
||||
const { resolver, cleanup } = makeResolver({
|
||||
permission: { external_directory: { "*": "ask" } },
|
||||
});
|
||||
try {
|
||||
const result = describeExternalDirectoryGate(
|
||||
readTcc(),
|
||||
[],
|
||||
resolver,
|
||||
new PathNormalizer(pathFlavorForPlatform(process.platform), cwd),
|
||||
);
|
||||
expect(isGateDescriptor(result)).toBe(true);
|
||||
expect((result as GateDescriptor).preCheck?.state).toBe("ask");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("allows a bash command referencing the typed (symlinked) path", async () => {
|
||||
const { resolver, cleanup } = makeResolver({
|
||||
permission: {
|
||||
external_directory: { "*": "ask", [`${linkDir}/*`]: "allow" },
|
||||
},
|
||||
});
|
||||
try {
|
||||
const command = `cat ${join(linkDir, "file.ts")}`;
|
||||
const tcc: ToolCallContext = {
|
||||
toolName: "bash",
|
||||
agentName: null,
|
||||
input: { command },
|
||||
toolCallId: "tc-2",
|
||||
cwd,
|
||||
};
|
||||
const program = await BashProgram.parse(
|
||||
command,
|
||||
new PathNormalizer(pathFlavorForPlatform(process.platform), cwd),
|
||||
);
|
||||
const result = describeBashExternalDirectoryGate(tcc, program, resolver);
|
||||
// All external paths are covered by the allow → bypass, no prompt.
|
||||
expect(isGateBypass(result)).toBe(true);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user