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
@@ -28,7 +28,6 @@ import {
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SUBAGENT_CHILD_SESSION_CREATED } from "#src/authority/subagent-lifecycle-events";
import { getSubagentSessionRegistry } from "#src/authority/subagent-registry";
import { getGlobalConfigPath } from "#src/config-paths";
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
import piPermissionSystemExtension from "#src/index";
import { PERMISSIONS_READY_CHANNEL } from "#src/permission-events";
@@ -55,9 +54,30 @@ const EXPECTED_HANDLERS = [
let agentDir: string;
/**
* Policy and hard-guard files live under the per-test temp dir and are passed
* to the factory explicitly — the bundle `config/` files are never touched.
*/
function testPolicyPath(): string {
return join(agentDir, "pi-permission-policy.jsonc");
}
function testHardGuardsPath(): string {
return join(agentDir, "pi-permission-hard-guards.jsonc");
}
function testConfigPaths(): { configPath: string; hardGuardConfigPath: string } {
return {
configPath: testPolicyPath(),
hardGuardConfigPath: testHardGuardsPath(),
};
}
beforeEach(() => {
agentDir = mkdtempSync(join(tmpdir(), "pi-perm-comp-root-"));
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
writeFileSync(
testHardGuardsPath(),
JSON.stringify({ version: 1, permission: { "*": "allow" } }),
);
});
afterEach(() => {
@@ -75,9 +95,9 @@ afterEach(() => {
// ── Shared helpers ──────────────────────────────────────────────────────────
/** Write the global config file under the stubbed agent dir. */
/** Write the global policy file under the stubbed temp dir. */
function writeGlobalConfig(config: Record<string, unknown>): void {
const globalConfigPath = getGlobalConfigPath(agentDir);
const globalConfigPath = testPolicyPath();
mkdirSync(dirname(globalConfigPath), { recursive: true });
writeFileSync(
globalConfigPath,
@@ -174,7 +194,7 @@ function fireSessionStart(
describe("event-handler registration completeness", () => {
it("registers a handler for every required event exactly once", () => {
const pi = makeFakePi();
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
expect([...pi.handlers.keys()].sort()).toEqual(EXPECTED_HANDLERS);
});
@@ -186,7 +206,7 @@ describe("shutdown teardown chain", () => {
it("unpublishes the service and unsubscribes the lifecycle on shutdown", async () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-teardown-cwd-"));
const pi = makeFakePi();
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
// The service is published at session_start, not at factory init.
await fireSessionStart(pi, makeChildCtx(cwd, "top-session"));
@@ -219,7 +239,7 @@ describe("service and gate share one formatter registry", () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-ui-cwd-"));
const pi = makeFakePi({ toolNames: ["demo"] });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const capturedTitles: string[] = [];
const { ctx } = makeUiCtx(cwd, capturedTitles);
@@ -257,7 +277,7 @@ describe("service and gate share one access extractor registry", () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-ext-cwd-"));
const pi = makeFakePi({ toolNames: ["ffgrep"] });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const { ctx } = makeUiCtx(cwd, []);
await fireSessionStart(pi, ctx);
@@ -295,7 +315,7 @@ describe("service and chain share one authorizer registry", () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-auth-cwd-"));
const pi = makeFakePi({ toolNames: ["demo"] });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const capturedTitles: string[] = [];
const { ctx } = makeUiCtx(cwd, capturedTitles);
@@ -327,7 +347,7 @@ describe("service and chain share one authorizer registry", () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-auth-optin-"));
const pi = makeFakePi({ toolNames: ["demo"] });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const capturedTitles: string[] = [];
const { ctx } = makeUiCtx(cwd, capturedTitles);
@@ -364,7 +384,7 @@ describe("ready emitted after service publication", () => {
seen.push(getPermissionsService() ? "present" : "missing");
});
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
// ready is not emitted at load; only after session_start publishes.
expect(seen).toEqual([]);
@@ -389,7 +409,7 @@ describe("single source of truth for session state", () => {
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-sot-cwd-"));
const pi = makeFakePi({ toolNames: ["demo"] });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
// UI ctx that approves the gate prompt for this session (options[1]).
// Return the second option label-agnostically — always the "for this
@@ -436,7 +456,7 @@ describe("service path queries evaluate the supplied path (#503)", () => {
writeGlobalConfig({ permission: { path: { [target]: "deny" } } });
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
await fireSessionStart(pi, makeChildCtx(cwd, "svc-path-session"));
const result = getPermissionsService()!.checkPermission("path", target);
@@ -453,7 +473,7 @@ describe("project trust gates project-scoped config (#644)", () => {
writeProjectConfig(cwd, { permission: { bash: "allow" } });
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
await fireSessionStart(
pi,
makeBaseCtx(cwd, "untrusted-session", { isProjectTrusted: false }),
@@ -473,7 +493,7 @@ describe("project trust gates project-scoped config (#644)", () => {
writeProjectConfig(cwd, { permission: { bash: "allow" } });
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
await fireSessionStart(
pi,
makeBaseCtx(cwd, "trusted-session", { isProjectTrusted: true }),
@@ -516,7 +536,7 @@ describe("bash bare-token path gating (#509, #645)", () => {
});
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const ctx = makeChildCtx(cwd, "bare-token-session-deny");
await fireSessionStart(pi, ctx);
@@ -534,7 +554,7 @@ describe("bash bare-token path gating (#509, #645)", () => {
});
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const ctx = makeChildCtx(cwd, "bare-token-session-wildcard");
await fireSessionStart(pi, ctx);
@@ -556,7 +576,7 @@ describe("bash bare-token path gating (#509, #645)", () => {
});
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const ctx = makeChildCtx(cwd, "bare-symlink-session-deny");
await fireSessionStart(pi, ctx);
@@ -573,7 +593,7 @@ describe("bash bare-token path gating (#509, #645)", () => {
});
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const ctx = makeChildCtx(cwd, "bare-token-session-unaffected");
await fireSessionStart(pi, ctx);
@@ -592,7 +612,7 @@ describe("bash bare-token path gating (#509, #645)", () => {
});
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const ctx = makeChildCtx(cwd, "bare-token-session-absent");
await fireSessionStart(pi, ctx);
@@ -613,7 +633,7 @@ describe("bash bare-token path gating (#509, #645)", () => {
});
const pi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const ctx = makeChildCtx(cwd, "bare-token-session-norule");
await fireSessionStart(pi, ctx);
@@ -637,9 +657,9 @@ describe("multi-instance global service interplay", () => {
const childSessionId = "child-session-mi";
const parentPi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(parentPi as unknown as ExtensionAPI);
piPermissionSystemExtension(parentPi as unknown as ExtensionAPI, testConfigPaths());
const childPi = makeFakePi({ events: createEventBus() });
piPermissionSystemExtension(childPi as unknown as ExtensionAPI);
piPermissionSystemExtension(childPi as unknown as ExtensionAPI, testConfigPaths());
// The parent is not a registered child, so it publishes its service.
await fireSessionStart(
@@ -703,7 +723,7 @@ describe("session approvals do not leak across same-cwd session switches", () =>
// ── Session #1: approve `demo` for the session, then shut down ──────────
const firstPi = makeFakePi({ toolNames: ["demo"] });
piPermissionSystemExtension(firstPi as unknown as ExtensionAPI);
piPermissionSystemExtension(firstPi as unknown as ExtensionAPI, testConfigPaths());
const firstCtx = makeSessionApprovingCtx(cwd, "switch-session-1");
await fireSessionStart(firstPi, firstCtx);
@@ -724,7 +744,7 @@ describe("session approvals do not leak across same-cwd session switches", () =>
// ── Session #2: the re-invoked cached factory, same cwd ────────────────
const secondPi = makeFakePi({ toolNames: ["demo"] });
piPermissionSystemExtension(secondPi as unknown as ExtensionAPI);
piPermissionSystemExtension(secondPi as unknown as ExtensionAPI, testConfigPaths());
await fireSessionStart(secondPi, makeChildCtx(cwd, "switch-session-2"));
@@ -776,12 +796,12 @@ describe("forwarded grant-scope selection round-trip", () => {
const parentBus = createEventBus();
const parentPi = makeFakePi({ events: parentBus, toolNames: ["demo"] });
piPermissionSystemExtension(parentPi as unknown as ExtensionAPI);
piPermissionSystemExtension(parentPi as unknown as ExtensionAPI, testConfigPaths());
const childPi = makeFakePi({
events: createEventBus(),
toolNames: ["demo"],
});
piPermissionSystemExtension(childPi as unknown as ExtensionAPI);
piPermissionSystemExtension(childPi as unknown as ExtensionAPI, testConfigPaths());
// The parent starts serving (its poll drains the inbox using the UI ctx).
const parentCtx = makeServingCtx(
@@ -841,12 +861,12 @@ describe("forwarded grant-scope selection round-trip", () => {
const parentBus = createEventBus();
const parentPi = makeFakePi({ events: parentBus, toolNames: ["demo"] });
piPermissionSystemExtension(parentPi as unknown as ExtensionAPI);
piPermissionSystemExtension(parentPi as unknown as ExtensionAPI, testConfigPaths());
const childPi = makeFakePi({
events: createEventBus(),
toolNames: ["demo"],
});
piPermissionSystemExtension(childPi as unknown as ExtensionAPI);
piPermissionSystemExtension(childPi as unknown as ExtensionAPI, testConfigPaths());
const parentCtx = makeServingCtx(
parentCwd,
@@ -905,7 +925,7 @@ describe("yolo grants asks synthesized after resolution", () => {
writeGlobalConfig(config);
const cwd = mkdtempSync(join(tmpdir(), "pi-perm-yolo-cwd-"));
const pi = makeFakePi({ toolNames: ["bash"] });
piPermissionSystemExtension(pi as unknown as ExtensionAPI);
piPermissionSystemExtension(pi as unknown as ExtensionAPI, testConfigPaths());
const prompts: string[] = [];
const { ctx } = makeUiCtx(cwd, prompts);