Files
云服务部-叶林立andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2aa1be0b54 feat: 组合包配置以包内 config/ 为唯一来源,并新增受保护写入人工审批
配置统一:权限策略、硬防御、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>
2026-10-20 01:05:31 -07:00

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");
});
});
});