refactor: 移除 context-mode,Chrome 快照改为直接返回

context-mode 的 ctx_execute 系工具以真实 shell 执行且权限为 allow,绕过了
bash 策略与 protectedWrites;其每轮注入、独立 MCP 子进程与 skills 也与组合包
提示词单一所有权冲突。大输出职责已由 pi-overflow(入口截断)与
pi-compaction(压力遮蔽)承担:
- 删除 context-mode 依赖、wrapper/过滤器、skills,以及 ctx_* 的权限、
  tools.jsonc、路由规则、tool-search 常驻与 4 个分组
- better-sqlite3@12 仅为 context-mode 传递依赖,随之移除根 allowScripts
- pi-chrome:按用户决定移除快照隐私规则与 .pi/chrome-context 临时文件机制,
  快照 JSON 直接作为工具结果返回、details 只留小型元数据,超长由 pi-overflow
  截断并用 overflow_query 提取;授权生命周期不变

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-21 20:45:33 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 2a6bf8c929
commit 56cbc83296
36 changed files with 178 additions and 2142 deletions
+25 -20
View File
@@ -51,8 +51,8 @@ test("all package extensions load together without global registration conflicts
const packageJson = JSON.parse(await readFile(join(repositoryRoot, "package.json"), "utf8")) as {
dependencies: Record<string, string>;
files: string[];
pi: { extensions: string[] };
allowScripts: Record<string, boolean>;
pi: { extensions: string[]; skills?: string[] };
allowScripts?: Record<string, boolean>;
};
assert.ok(
packageJson.pi.extensions.includes("./extensions/codegraph.ts"),
@@ -213,14 +213,19 @@ test("all package extensions load together without global registration conflicts
"./pi-overflow/index.ts",
"pi-overflow bound stage must load after all other content extensions and before system-prompt",
);
assert.ok(
packageJson.pi.extensions.includes("./extensions/context-mode.ts"),
"Context Mode must load through the bundle's prompt-guarded wrapper",
assert.equal(
packageJson.dependencies["context-mode"],
undefined,
"context-mode was retired; new tool-result bounding is owned by pi-overflow",
);
assert.equal(
packageJson.pi.extensions.some((entry) => entry.includes("context-mode/build/")),
packageJson.pi.extensions.some((entry) => entry.includes("context-mode")),
false,
"the raw Context Mode adapter must not be loaded alongside the wrapper",
"no context-mode entry point may be loaded",
);
assert.ok(
!Array.isArray(packageJson.pi.skills) || packageJson.pi.skills.length === 0,
"pi.skills must be absent or empty now that the context-mode skills are gone",
);
assert.equal(
packageJson.pi.extensions.includes("./extensions/system-prompt-cover.ts"),
@@ -260,28 +265,28 @@ test("all package extensions load together without global registration conflicts
);
assert.equal(packageJson.dependencies["@the-forge-flow/hippo-memory-pi"], undefined);
assert.equal(packageJson.dependencies["better-sqlite3"], undefined);
// allowScripts must approve exactly the root lockfile's better-sqlite3
// build — and nothing else. pi-hermes-memory's own lockfile carries a
// nested major that ships N-API prebuilds and needs no approval entry.
// better-sqlite3 was only present as context-mode's transitive build
// dependency; with context-mode retired the root lockfile must not carry a
// root-level better-sqlite3 and allowScripts must not approve one.
// pi-hermes-memory's own lockfile carries a nested major that ships N-API
// prebuilds and needs no approval entry.
const rootLock = JSON.parse(await readFile(join(repositoryRoot, "package-lock.json"), "utf8")) as {
packages: Record<string, { version?: string }>;
};
const hermesLock = JSON.parse(await readFile(join(repositoryRoot, "pi-hermes-memory", "package-lock.json"), "utf8")) as {
packages: Record<string, { version?: string }>;
};
const rootSqliteVersion = rootLock.packages["node_modules/better-sqlite3"]?.version;
const hermesSqliteVersion = hermesLock.packages["node_modules/better-sqlite3"]?.version;
assert.match(rootSqliteVersion ?? "", /^\d+\.\d+\.\d+$/, "the root lockfile must pin a root-level better-sqlite3");
assert.equal(
rootLock.packages["node_modules/better-sqlite3"],
undefined,
"the root lockfile must not pin a root-level better-sqlite3 now that context-mode is gone",
);
assert.match(hermesSqliteVersion ?? "", /^\d+\.\d+\.\d+$/, "the Hermes lockfile must pin its own better-sqlite3");
assert.deepEqual(
Object.keys(packageJson.allowScripts).filter((key) => key.startsWith("better-sqlite3@")),
[`better-sqlite3@${rootSqliteVersion}`],
"allowScripts must approve exactly the root lockfile's better-sqlite3 build",
);
assert.equal(
packageJson.allowScripts[`better-sqlite3@${hermesSqliteVersion}`],
undefined,
`Hermes uses better-sqlite3 ${hermesSqliteVersion} N-API prebuilds, so no extra install-script approval is required`,
Object.keys(packageJson.allowScripts ?? {}).filter((key) => key.startsWith("better-sqlite3@")),
[],
"allowScripts must not approve a better-sqlite3 build without a root-level package",
);
assert.equal(packageJson.dependencies["@sinclair/typebox"], undefined);
const home = await mkdtemp(join(tmpdir(), "my-pi-extension-load-"));
+3 -69
View File
@@ -29,10 +29,6 @@ import systemPromptExtension, {
getSystemPromptDumpPath,
} from "../extensions/system-prompt.ts";
import { guardExtensionApi } from "../extensions/prompt-guard.ts";
import {
CONTEXT_MODE_ANCHOR_PREFIX,
filterContextModeMessages,
} from "../extensions/context-mode-filter.ts";
import { BUNDLE_GROUP_DEFINITIONS } from "../pi-tool-search/extensions/bundle-groups.ts";
import { BUNDLE_TOOL_SEARCH_DEFAULTS } from "../pi-tool-search/extensions/config.ts";
@@ -164,7 +160,6 @@ test("prompt config carries no retired guidance phrasing", () => {
"read/grep",
"replace_text",
"Read/edit files",
"ctx_fetch_and_index then ctx_search",
];
for (const file of ["preamble.md", "tools.jsonc", "routing.jsonc"]) {
const source = readFileSync(join(promptConfigDir, file), "utf8");
@@ -267,8 +262,8 @@ test("bundled routing rules select the same guidance the retired tool-routing ex
assert.match(ssh, /only when the user explicitly names an imported host/);
assert.match(ssh, /command-local directory change/);
const chrome = renderRouting(routing.rules, ["chrome_snapshot", "ctx_execute_file"]);
assert.match(chrome, /immediately analyze the returned workspace-relative capture path/);
const chrome = renderRouting(routing.rules, ["chrome_snapshot", "overflow_query"]);
assert.match(chrome, /return the page snapshot JSON directly/);
const minimal = renderRouting(routing.rules, ["read"]);
assert.doesNotMatch(minimal, /codegraph_explore first/);
@@ -646,7 +641,7 @@ test("prompt guard runs context results through the provided contextFilter", asy
test("prompt guard returns the handler result untouched when the filter changes nothing", async () => {
const { pi, handlers } = makeRecordingPi();
const guarded = guardExtensionApi(pi as unknown as ExtensionAPI, {
contextFilter: filterContextModeMessages,
contextFilter: (_before, after) => after,
});
const expected = { messages: [{ role: "user", content: "plain" }] };
@@ -662,67 +657,6 @@ test("prompt guard passes unrelated API members through to the real pi object",
assert.equal((guarded as unknown as { someOtherApi: () => string }).someOtherApi(), "passthrough");
});
// ── context-mode filter ─────────────────────────────────────────────────────
test("context-mode filter drops only the routing-anchor part", () => {
const before = [{ role: "user", content: "real" }];
const anchor = `${CONTEXT_MODE_ANCHOR_PREFIX} ctx_batch_execute > ctx_execute`;
const after = [
...before,
{ role: "user", content: `${anchor}\n\n<active_memory>\nx\n</active_memory>` },
];
const filtered = filterContextModeMessages(before, after);
assert.equal(filtered.length, 2);
assert.equal(filtered[1].content, "<active_memory>\nx\n</active_memory>");
});
test("context-mode filter removes the appended message when only the anchor remains", () => {
const before = [{ role: "user", content: "real" }];
const after = [
...before,
{ role: "user", content: `${CONTEXT_MODE_ANCHOR_PREFIX} only anchor` },
];
assert.deepEqual(filterContextModeMessages(before, after), before);
});
test("context-mode filter returns the same reference when nothing changed", () => {
const before = [{ role: "user", content: "real" }];
const after = [...before, { role: "user", content: "no anchor here" }];
assert.equal(filterContextModeMessages(before, after), after);
assert.equal(filterContextModeMessages(after, after), after);
});
test("context-mode filter ignores appended non-user or non-string messages", () => {
const before = [{ role: "user", content: "real" }];
const after = [
...before,
{ role: "assistant", content: `${CONTEXT_MODE_ANCHOR_PREFIX} not ours` },
{ role: "user", content: [{ type: "text", text: "structured" }] },
];
assert.equal(filterContextModeMessages(before, after), after);
});
// ── Pinned upstream text ────────────────────────────────────────────────────
test("context-mode upstream still appends the single anchor-bearing user message", async () => {
const packageJson = JSON.parse(
await readFile(join(repositoryRoot, "package.json"), "utf8"),
) as { dependencies: Record<string, string> };
assert.match(
packageJson.dependencies["context-mode"] ?? "",
/^\d+\.\d+\.\d+$/,
"the context-filter pin requires an exact upstream version",
);
const upstream = await readFile(
join(repositoryRoot, "node_modules/context-mode/build/adapters/pi/extension.js"),
"utf8",
);
assert.ok(upstream.includes('parts.push("context-mode active. Hierarchy:'));
assert.ok(upstream.includes('_pendingContext = extraParts.join("\\n\\n")'));
assert.ok(upstream.includes("event.messages.push({"));
assert.ok(upstream.includes('role: "user"'));
});
// ── /dump-system-prompt ─────────────────────────────────────────────────────
test("dump-system-prompt writes the last composed prompt under .pi-debug", async () => {
+2 -2
View File
@@ -5,7 +5,7 @@ import test from "node:test";
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")) as {
dependencies: Record<string, string>;
files: string[];
pi: { extensions: string[]; skills: string[] };
pi: { extensions: string[]; skills?: string[] };
};
const filesIncludePackageDir = (files: string[], packageDir: string): boolean =>
@@ -45,7 +45,7 @@ test("no extension in the bundle loads the MCP adapter or upstream search extens
});
test("the keenable skill path is gone with the upstream package", () => {
for (const skillPath of packageJson.pi.skills) {
for (const skillPath of packageJson.pi.skills ?? []) {
assert.equal(skillPath.includes("@keenable"), false);
}
});