mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55: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>
189 lines
6.8 KiB
TypeScript
189 lines
6.8 KiB
TypeScript
/**
|
|
* `before_agent_start` prompt injection (#251).
|
|
*
|
|
* pi 0.86+ passes a normalized *clone* of the prompt options and re-renders the
|
|
* prompt from it, so appending the memory context through `appendSystemPrompt`
|
|
* keeps the prompt a set of named sections for later handlers and section-aware
|
|
* providers; returning `systemPrompt` forces the rendered text instead.
|
|
*
|
|
* 0.80.6-0.85.x put `systemPromptOptions` on the event too, but they pass the raw
|
|
* long-lived base options and only apply a returned `systemPrompt` — a mutation
|
|
* there reaches no prompt and leaks into `ctx.getSystemPromptOptions()`. The two
|
|
* shapes below are the ones `agent-session.js` actually builds on each side of
|
|
* that boundary, so the branch is tested against the real inputs rather than a
|
|
* hand-built `{}` that no SDK version produces.
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import * as fs from "node:fs/promises";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { after, beforeEach, describe, it } from "node:test";
|
|
|
|
// Bind the entry point to a disposable agent root, never the user's memory.
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "pi-prompt-injection-"));
|
|
const previousRoot = process.env.PI_CODING_AGENT_DIR;
|
|
const previousConfigPath = process.env.PI_HERMES_MEMORY_CONFIG_PATH;
|
|
process.env.PI_CODING_AGENT_DIR = root;
|
|
process.env.PI_HERMES_MEMORY_CONFIG_PATH = path.join(root, "hermes-memory-config.json");
|
|
const { default: registerExtension } = await import("../src/index.js");
|
|
const globalDir = path.join(root, "pi-hermes-memory");
|
|
const cwd = path.join(root, "workspace");
|
|
|
|
/** Options as 0.86+ normalizes them: `sections` and `forceSystemPrompt` always present. */
|
|
function structuredOptions(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
customPrompt: undefined,
|
|
forceSystemPrompt: undefined,
|
|
selectedTools: ["read"],
|
|
toolSnippets: {},
|
|
toolGuidelines: {},
|
|
promptGuidelines: [],
|
|
appendSystemPrompt: "",
|
|
sections: {},
|
|
cwd,
|
|
contextFiles: [],
|
|
skills: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Options as 0.80.x-0.85.x build them: the raw base object, no `sections`. */
|
|
function legacyOptions(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
cwd,
|
|
skills: [],
|
|
contextFiles: [],
|
|
customPrompt: undefined,
|
|
appendSystemPrompt: "",
|
|
selectedTools: ["read"],
|
|
toolSnippets: {},
|
|
promptGuidelines: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
let handlers: Record<string, Array<(event: any, ctx: any) => any>>;
|
|
let ctx: any;
|
|
|
|
async function configure(overrides: Record<string, unknown> = {}) {
|
|
await fs.writeFile(path.join(root, "hermes-memory-config.json"), JSON.stringify({
|
|
memoryMode: "policy-only",
|
|
memoryPolicyStyle: "full",
|
|
lazyInitialization: true,
|
|
reviewEnabled: false,
|
|
correctionDetection: false,
|
|
flushOnCompact: false,
|
|
flushOnShutdown: false,
|
|
...overrides,
|
|
}));
|
|
}
|
|
|
|
function register() {
|
|
registerExtension({
|
|
on(event: string, handler: any) { (handlers[event] ??= []).push(handler); },
|
|
registerTool() {},
|
|
registerCommand() {},
|
|
} as any);
|
|
}
|
|
|
|
async function emit(event: string, data: any = {}) {
|
|
let result: any;
|
|
for (const handler of handlers[event] ?? []) result = (await handler(data, ctx)) ?? result;
|
|
return result;
|
|
}
|
|
|
|
async function registerWith(overrides: Record<string, unknown> = {}) {
|
|
handlers = {};
|
|
await configure(overrides);
|
|
register();
|
|
await emit("session_start");
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await fs.mkdir(globalDir, { recursive: true });
|
|
await fs.mkdir(cwd, { recursive: true });
|
|
ctx = {
|
|
cwd,
|
|
hasUI: false,
|
|
sessionManager: { getBranch: () => [], getSessionFile: () => undefined },
|
|
ui: { notify: () => {} },
|
|
};
|
|
});
|
|
|
|
after(async () => {
|
|
if (previousRoot === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
else process.env.PI_CODING_AGENT_DIR = previousRoot;
|
|
if (previousConfigPath === undefined) delete process.env.PI_HERMES_MEMORY_CONFIG_PATH;
|
|
else process.env.PI_HERMES_MEMORY_CONFIG_PATH = previousConfigPath;
|
|
try {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
} catch {
|
|
// Windows may keep the directory busy briefly; the temp dir is disposable.
|
|
}
|
|
});
|
|
|
|
describe("before_agent_start prompt injection", () => {
|
|
it("appends through structured prompt options instead of forcing the prompt", async () => {
|
|
await registerWith();
|
|
const event = { systemPrompt: "base prompt", systemPromptOptions: structuredOptions() };
|
|
|
|
const result = await emit("before_agent_start", event);
|
|
|
|
assert.equal(result, undefined, "structured events must not force a full prompt replacement");
|
|
assert.match(event.systemPromptOptions.appendSystemPrompt, /memory-policy/);
|
|
});
|
|
|
|
it("appends after an earlier handler's appendSystemPrompt", async () => {
|
|
await registerWith();
|
|
const event = {
|
|
systemPrompt: "base prompt",
|
|
systemPromptOptions: structuredOptions({ appendSystemPrompt: "earlier handler text" }),
|
|
};
|
|
|
|
await emit("before_agent_start", event);
|
|
|
|
const appended = event.systemPromptOptions.appendSystemPrompt;
|
|
assert.match(appended, /earlier handler text/);
|
|
assert.match(appended, /memory-policy/);
|
|
assert.ok(
|
|
appended.indexOf("earlier handler text") < appended.indexOf("memory-policy"),
|
|
"the policy goes after earlier contributions",
|
|
);
|
|
});
|
|
|
|
it("reaches the 0.80.x session through the returned prompt, not the options object", async () => {
|
|
await registerWith();
|
|
const options = legacyOptions();
|
|
const event = { systemPrompt: "base prompt", systemPromptOptions: options };
|
|
|
|
const result = await emit("before_agent_start", event);
|
|
|
|
// 0.80.x-0.85.x apply `result.systemPrompt` or reset to the base prompt
|
|
// (agent-session.js); the options object is never rendered, and it is the one
|
|
// `ctx.getSystemPromptOptions()` hands to other extensions.
|
|
const applied = result?.systemPrompt ?? event.systemPrompt;
|
|
assert.match(applied, /memory-policy/, "the policy must reach a prompt the session applies");
|
|
assert.equal(options.appendSystemPrompt, "", "a legacy event must not accumulate into the exposed options");
|
|
});
|
|
|
|
it("keeps the replacement path when the event carries no options at all", async () => {
|
|
await registerWith();
|
|
const result = await emit("before_agent_start", { systemPrompt: "base prompt" });
|
|
|
|
assert.match(result.systemPrompt, /base prompt/);
|
|
assert.match(result.systemPrompt, /memory-policy/);
|
|
});
|
|
|
|
it("leaves the options untouched when there is no context to inject", async () => {
|
|
await registerWith({ memoryPolicyStyle: "none" });
|
|
const options = structuredOptions();
|
|
const snapshot = structuredClone(options);
|
|
const event = { systemPrompt: "base prompt", systemPromptOptions: options };
|
|
|
|
const result = await emit("before_agent_start", event);
|
|
|
|
assert.equal(result, undefined);
|
|
assert.deepEqual(event.systemPromptOptions, snapshot);
|
|
});
|
|
});
|