mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
配置统一:权限策略、硬防御、Tool Search、提示词封面以及新迁入的 RTK、pi-compaction、pi-worktree、pi-hermes-memory 配置全部由扩展 直接读取已安装组合包内的 config/,pi install / pi update 即完成部署; 删除 rebuild-bundle-config.mjs 与 bundle-manifest.json,my-pi/ 只保留 日志与缓存。update.sh 成功升级后将默认 git 来源的已安装副本 reset 回发布版本。AGENTS.md 新增「组合包配置原则」。 脚本整合:search_config.sh / ssh_config.sh 合并为 configure.sh search|ssh;install.sh / update.sh 共享函数抽到 scripts/lib.sh,远程 安装器改为先下载到临时文件再执行,下载失败可被检测。 权限:hard-guards 新增 protectedWrites,写入 Pi 凭据/设置、已安装 组合包与扩展目录、pi-ssh 凭据库时一律人工确认(跳过 AutoReview、 yolo 与 Session grant,无 UI 时拒绝);放行 ls、LSP 查询、ctx 只读、 web_search、Chrome 只读与 git 只读命令,skill_manage 改为 ask。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
249 lines
8.3 KiB
TypeScript
249 lines
8.3 KiB
TypeScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { homedir, tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { HardGuardPolicyLoader } from "#src/hard-guard-loader";
|
|
|
|
const roots: string[] = [];
|
|
|
|
const ENV_KEYS = ["PI_CODING_AGENT_DIR", "XDG_CONFIG_HOME"] as const;
|
|
const savedEnv: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>> =
|
|
{};
|
|
|
|
afterEach(() => {
|
|
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
for (const key of ENV_KEYS) {
|
|
if (savedEnv[key] === undefined) delete process.env[key];
|
|
else process.env[key] = savedEnv[key];
|
|
delete savedEnv[key];
|
|
}
|
|
});
|
|
|
|
function setEnv(key: (typeof ENV_KEYS)[number], value: string | undefined) {
|
|
if (!(key in savedEnv)) savedEnv[key] = process.env[key];
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
|
|
describe("HardGuardPolicyLoader", () => {
|
|
it("loads only the global JSONC hard-guard file and preserves allow exceptions", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "hard-guard-loader-"));
|
|
roots.push(root);
|
|
const userPath = join(root, "hard-guards.jsonc");
|
|
mkdirSync(root, { recursive: true });
|
|
writeFileSync(userPath, `{
|
|
// Allow public examples, deny the rest of the credentials tree.
|
|
"version": 1,
|
|
"permission": {
|
|
"*": "allow",
|
|
"~/.ssh/**": "deny",
|
|
"~/.ssh/*.example": "allow"
|
|
}
|
|
}`);
|
|
|
|
const loader = new HardGuardPolicyLoader(userPath);
|
|
const config = loader.loadGlobalConfig();
|
|
|
|
expect(config.permission).toEqual({
|
|
"*": "allow",
|
|
"~/.ssh/**": "deny",
|
|
"~/.ssh/*.example": "allow",
|
|
});
|
|
expect(loader.getConfigIssues()).toEqual([]);
|
|
});
|
|
|
|
it("fails closed with an issue when the file is missing", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "hard-guard-loader-"));
|
|
roots.push(root);
|
|
const userPath = join(root, "hard-guards.jsonc");
|
|
|
|
const loader = new HardGuardPolicyLoader(userPath);
|
|
|
|
expect(loader.loadGlobalConfig().permission).toEqual({ "*": "deny" });
|
|
expect(loader.getConfigIssues().join("\n")).toContain("denying all");
|
|
});
|
|
|
|
it("fails closed when the file is invalid", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "hard-guard-loader-"));
|
|
roots.push(root);
|
|
const userPath = join(root, "hard-guards.jsonc");
|
|
writeFileSync(userPath, `{ "version": 1, "permission": { "*": "ask" } }`);
|
|
|
|
const loader = new HardGuardPolicyLoader(userPath);
|
|
|
|
expect(loader.loadGlobalConfig().permission).toEqual({ "*": "deny" });
|
|
expect(loader.getConfigIssues().join("\n")).toContain("allow or deny");
|
|
});
|
|
|
|
it("rejects an unsupported version instead of interpreting it", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "hard-guard-loader-"));
|
|
roots.push(root);
|
|
const userPath = join(root, "hard-guards.jsonc");
|
|
writeFileSync(
|
|
userPath,
|
|
`{ "version": 2, "permission": { "*": "allow" } }`,
|
|
);
|
|
|
|
const loader = new HardGuardPolicyLoader(userPath);
|
|
|
|
expect(loader.loadGlobalConfig().permission).toEqual({ "*": "deny" });
|
|
expect(loader.getConfigIssues().join("\n")).toContain(
|
|
"unsupported hard guard version",
|
|
);
|
|
});
|
|
|
|
it("keeps the last good snapshot when the file turns invalid", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "hard-guard-loader-"));
|
|
roots.push(root);
|
|
const userPath = join(root, "hard-guards.jsonc");
|
|
writeFileSync(
|
|
userPath,
|
|
`{ "version": 1, "permission": { "*": "allow", "~/.ssh/**": "deny" } }`,
|
|
);
|
|
|
|
const loader = new HardGuardPolicyLoader(userPath);
|
|
const first = loader.loadGlobalConfig();
|
|
expect(first.permission).toEqual({
|
|
"*": "allow",
|
|
"~/.ssh/**": "deny",
|
|
});
|
|
|
|
// A corrupt edit must not weaken the boundary: the last valid snapshot
|
|
// keeps applying while the file stays broken.
|
|
writeFileSync(userPath, "{ not json");
|
|
const second = loader.loadGlobalConfig();
|
|
|
|
expect(second).toEqual(first);
|
|
expect(loader.getConfigIssues().join("\n")).toContain("Failed to load");
|
|
});
|
|
|
|
it("restores the file's own rules once it becomes valid again", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "hard-guard-loader-"));
|
|
roots.push(root);
|
|
const userPath = join(root, "hard-guards.jsonc");
|
|
writeFileSync(
|
|
userPath,
|
|
`{ "version": 1, "permission": { "*": "allow", "~/.ssh/**": "deny" } }`,
|
|
);
|
|
|
|
const loader = new HardGuardPolicyLoader(userPath);
|
|
loader.loadGlobalConfig();
|
|
writeFileSync(userPath, "{ not json");
|
|
loader.loadGlobalConfig();
|
|
|
|
writeFileSync(
|
|
userPath,
|
|
`{ "version": 1, "permission": { "*": "allow", "~/.aws/**": "deny" } }`,
|
|
);
|
|
const restored = loader.loadGlobalConfig();
|
|
|
|
expect(restored.permission).toEqual({
|
|
"*": "allow",
|
|
"~/.aws/**": "deny",
|
|
});
|
|
});
|
|
|
|
describe("protectedWrites", () => {
|
|
function writeConfig(userPath: string, protectedWrites: unknown): void {
|
|
writeFileSync(
|
|
userPath,
|
|
JSON.stringify({
|
|
version: 1,
|
|
permission: { "*": "allow" },
|
|
protectedWrites,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function makeLoader() {
|
|
const root = mkdtempSync(join(tmpdir(), "hard-guard-loader-"));
|
|
roots.push(root);
|
|
const userPath = join(root, "hard-guards.jsonc");
|
|
return { root, userPath, loader: new HardGuardPolicyLoader(userPath) };
|
|
}
|
|
|
|
it("expands {agentDir} honoring PI_CODING_AGENT_DIR", () => {
|
|
setEnv("PI_CODING_AGENT_DIR", "/tmp/fake-agent-dir");
|
|
const { userPath, loader } = makeLoader();
|
|
writeConfig(userPath, [
|
|
{ pattern: "{agentDir}/auth.json", label: "Pi 凭据" },
|
|
]);
|
|
|
|
expect(loader.getProtectedWrites()).toEqual([
|
|
{ pattern: "/tmp/fake-agent-dir/auth.json", label: "Pi 凭据" },
|
|
]);
|
|
});
|
|
|
|
it("expands {configHome} honoring XDG_CONFIG_HOME", () => {
|
|
setEnv("XDG_CONFIG_HOME", "/tmp/fake-config-home");
|
|
const { userPath, loader } = makeLoader();
|
|
writeConfig(userPath, [
|
|
{ pattern: "{configHome}/my-pi/**", label: "pi-ssh 凭据库" },
|
|
]);
|
|
|
|
expect(loader.getProtectedWrites()).toEqual([
|
|
{ pattern: "/tmp/fake-config-home/my-pi/**", label: "pi-ssh 凭据库" },
|
|
]);
|
|
});
|
|
|
|
it("falls back to ~/.config for {configHome} and expands leading ~/", () => {
|
|
setEnv("XDG_CONFIG_HOME", undefined);
|
|
const { userPath, loader } = makeLoader();
|
|
writeConfig(userPath, [
|
|
{ pattern: "{configHome}/my-pi/**", label: "xdg" },
|
|
{ pattern: "~/secret.txt", label: "home" },
|
|
]);
|
|
|
|
expect(loader.getProtectedWrites()).toEqual([
|
|
{ pattern: join(homedir(), ".config", "my-pi/**"), label: "xdg" },
|
|
{ pattern: join(homedir(), "secret.txt"), label: "home" },
|
|
]);
|
|
});
|
|
|
|
it("returns empty rules when no protectedWrites key exists", () => {
|
|
const { userPath, loader } = makeLoader();
|
|
writeFileSync(
|
|
userPath,
|
|
`{ "version": 1, "permission": { "*": "allow" } }`,
|
|
);
|
|
|
|
expect(loader.getProtectedWrites()).toEqual([]);
|
|
expect(loader.getConfigIssues()).toEqual([]);
|
|
});
|
|
|
|
it("returns empty rules when the file is missing (requests deny anyway)", () => {
|
|
const { loader } = makeLoader();
|
|
|
|
expect(loader.getProtectedWrites()).toEqual([]);
|
|
expect(loader.getConfigIssues().join("\n")).toContain("denying all");
|
|
});
|
|
|
|
it("treats a malformed protectedWrites as an invalid file", () => {
|
|
const { userPath, loader } = makeLoader();
|
|
writeConfig(userPath, { pattern: "/x", label: "missing array" });
|
|
|
|
expect(loader.loadGlobalConfig().permission).toEqual({ "*": "deny" });
|
|
expect(loader.getConfigIssues().join("\n")).toContain(
|
|
"protectedWrites",
|
|
);
|
|
});
|
|
|
|
it("keeps the last good protectedWrites when the file turns invalid", () => {
|
|
const { userPath, loader } = makeLoader();
|
|
writeConfig(userPath, [
|
|
{ pattern: "/vault/auth.json", label: "Pi 凭据" },
|
|
]);
|
|
loader.loadGlobalConfig();
|
|
|
|
writeFileSync(userPath, "{ definitely not json at all");
|
|
loader.loadGlobalConfig();
|
|
|
|
expect(loader.getProtectedWrites()).toEqual([
|
|
{ pattern: "/vault/auth.json", label: "Pi 凭据" },
|
|
]);
|
|
expect(loader.getConfigIssues().join("\n")).toContain("Failed to load");
|
|
});
|
|
});
|
|
});
|