Files
my-pi/extensions/prompt-guard.ts
T
云服务部-叶林立andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> ba164896d2 feat: 系统提示词改为单一入口统一管理
多个扩展各自整段改写系统提示词并按加载顺序接力,任一扩展定死提示词后
其余只能做字符串手术,且各上游的工具说明与组合包实际工具格局冲突。

- 新增 extensions/system-prompt.ts 作为唯一出口(加载列表最后),按
  config/prompt/(preamble.md、tools.jsonc、routing.jsonc、sections.jsonc)
  组合开场白、工具简介/规则与 <tool_routing> 分区;替代原封面与
  tool-routing 扩展,/dump-system-prompt 输出最终提示词。
- pi-chrome、pi-ssh、pi-hermes-memory 改为写命名分区,pi-rtk-optimizer
  改写 promptGuidelines,pi-permission-system 直接过滤结构化 skills,
  均不再返回 systemPrompt;pi-chrome 未授权时不再每轮冻结提示词。
- 新增 prompt-guard 代理加载第三方扩展(Hashline、FFF、LSP、
  context-mode):拦截直接改写提示词并按目录覆盖工具说明;context-mode
  去掉与组合包路由冲突的锚点段,保留记忆与恢复快照。
- 消除冲突:bash 简介不再建议 grep/find;Hashline edit 不再称可用 grep
  锚点、不再介绍已关闭的 replace_text;context-mode 读写/网页路由提示。
- 守门测试:工具目录覆盖率、禁用用法检查、context-mode 上游原文锁定;
  AGENTS.md 新增「系统提示词单一所有权」。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-20 17:53:04 -07:00

100 lines
3.7 KiB
TypeScript

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { loadToolCatalog, type ToolCatalog } from "./system-prompt-config.ts";
type AnyMessage = { role?: string; content?: unknown } & Record<string, unknown>;
type AnyHandler = (event: never, ctx: never) => unknown;
export interface PromptGuardOptions {
/**
* Post-process the messages a wrapped `context` handler leaves behind.
* Receives the message list snapshot from before the handler ran and the
* list after; returns the list the handler's result should report.
*/
contextFilter?: (before: AnyMessage[], after: AnyMessage[]) => AnyMessage[];
}
/**
* Proxy an `ExtensionAPI` for third-party extensions so they cannot bypass the
* bundle's single system-prompt owner (`extensions/system-prompt.ts`):
*
* - `registerTool` applies the tool's `description` override from
* `config/prompt/tools.jsonc` when the catalog carries one (lazily loaded;
* a catalog error keeps the upstream description).
* - `before_agent_start` handlers keep every returned field except
* `systemPrompt`, which is dropped.
* - `context` handlers are post-processed through `opts.contextFilter`.
* - Everything else passes through unchanged.
*/
export function guardExtensionApi(pi: ExtensionAPI, opts: PromptGuardOptions = {}): ExtensionAPI {
let catalogCache: ToolCatalog | null | undefined;
const catalog = (): ToolCatalog | null => {
if (catalogCache === undefined) {
try {
catalogCache = loadToolCatalog();
} catch {
catalogCache = null;
}
}
return catalogCache;
};
const registerTool: ExtensionAPI["registerTool"] = (tool) => {
const entry = catalog()?.tools[tool.name];
if (entry !== undefined && !("inherit" in entry) && typeof entry.description === "string") {
pi.registerTool({ ...tool, description: entry.description });
} else {
pi.registerTool(tool);
}
};
const on: ExtensionAPI["on"] = ((event: string, handler: AnyHandler): unknown => {
let wrapped = handler;
if (event === "before_agent_start") {
wrapped = (async (eventPayload: unknown, ctx: unknown) => {
const result = await handler(eventPayload as never, ctx as never);
if (result !== null && typeof result === "object" && "systemPrompt" in result) {
const rest = { ...(result as Record<string, unknown>) };
delete rest.systemPrompt;
return rest;
}
return result;
}) as AnyHandler;
} else if (event === "context" && opts.contextFilter) {
const filter = opts.contextFilter;
wrapped = (async (eventPayload: { messages?: AnyMessage[] }, ctx: unknown) => {
const messages = Array.isArray(eventPayload.messages) ? eventPayload.messages : [];
const before = messages.slice();
const result = await handler(eventPayload as never, ctx as never);
const resultMessages =
result !== null &&
typeof result === "object" &&
Array.isArray((result as { messages?: unknown }).messages)
? ((result as { messages: AnyMessage[] }).messages)
: messages;
const filtered = filter(before, resultMessages);
if (filtered === resultMessages) {
return result;
}
return {
...(result !== null && typeof result === "object" ? (result as Record<string, unknown>) : {}),
messages: filtered,
};
}) as AnyHandler;
}
// `on` is overloaded per event name; drop to a loosely typed call and
// forward whatever the real registration returns (e.g. a disposer).
return (pi.on as (name: string, h: AnyHandler) => unknown)(event, wrapped);
}) as ExtensionAPI["on"];
return new Proxy(pi, {
get(target, prop, receiver) {
if (prop === "registerTool") return registerTool;
if (prop === "on") return on;
const value = Reflect.get(target, prop, receiver);
return typeof value === "function" ? value.bind(target) : value;
},
});
}