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>
This commit is contained in:
云服务部-叶林立
2026-10-20 01:05:31 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent a0ee9389c7
commit 2aa1be0b54
105 changed files with 2337 additions and 1413 deletions
@@ -1,5 +1,5 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
@@ -7,10 +7,25 @@ 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-"));
@@ -27,7 +42,7 @@ describe("HardGuardPolicyLoader", () => {
}
}`);
const loader = new HardGuardPolicyLoader(userPath, join(root, "missing-fallback.jsonc"));
const loader = new HardGuardPolicyLoader(userPath);
const config = loader.loadGlobalConfig();
expect(config.permission).toEqual({
@@ -38,13 +53,24 @@ describe("HardGuardPolicyLoader", () => {
expect(loader.getConfigIssues()).toEqual([]);
});
it("fails closed when neither the user file nor fallback is valid", () => {
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, join(root, "missing-fallback.jsonc"));
const loader = new HardGuardPolicyLoader(userPath);
expect(loader.loadGlobalConfig().permission).toEqual({ "*": "deny" });
expect(loader.getConfigIssues().join("\n")).toContain("allow or deny");
@@ -59,10 +85,7 @@ describe("HardGuardPolicyLoader", () => {
`{ "version": 2, "permission": { "*": "allow" } }`,
);
const loader = new HardGuardPolicyLoader(
userPath,
join(root, "missing-fallback.jsonc"),
);
const loader = new HardGuardPolicyLoader(userPath);
expect(loader.loadGlobalConfig().permission).toEqual({ "*": "deny" });
expect(loader.getConfigIssues().join("\n")).toContain(
@@ -79,10 +102,7 @@ describe("HardGuardPolicyLoader", () => {
`{ "version": 1, "permission": { "*": "allow", "~/.ssh/**": "deny" } }`,
);
const loader = new HardGuardPolicyLoader(
userPath,
join(root, "missing-fallback.jsonc"),
);
const loader = new HardGuardPolicyLoader(userPath);
const first = loader.loadGlobalConfig();
expect(first.permission).toEqual({
"*": "allow",
@@ -107,10 +127,7 @@ describe("HardGuardPolicyLoader", () => {
`{ "version": 1, "permission": { "*": "allow", "~/.ssh/**": "deny" } }`,
);
const loader = new HardGuardPolicyLoader(
userPath,
join(root, "missing-fallback.jsonc"),
);
const loader = new HardGuardPolicyLoader(userPath);
loader.loadGlobalConfig();
writeFileSync(userPath, "{ not json");
loader.loadGlobalConfig();
@@ -126,4 +143,106 @@ describe("HardGuardPolicyLoader", () => {
"~/.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");
});
});
});