feat: 接入 Pi 内置 codemode,并修复全面审查发现的问题

- codemode:Tool Search 常驻(alwaysEnabled)、权限 allow(嵌套调用仍逐个过权限门)、
  tools.jsonc 登记使用规则;RTK 跳过 parentToolCallId 嵌套调用,避免丢失 bash
  structuredContent;Tool Search 目录过滤 hidden / codemode(script-only)exposure;
  扩展加载测试加载 builtin:codemode 并断言其常驻激活
- 搜索 key 不再经命令行参数传递:search-auth.mjs 改为 --set-stdin(先读校验再加锁),
  configure.sh 交互输入经管道、非交互用 --<provider> -
- CodeGraph 客户端监听子进程 stdin error,避免 EPIPE 未处理导致崩溃
- 移除 PI_TOOL_SEARCH/HERMES_MEMORY/RTK_CONFIG_PATH 环境变量入口,测试改用工厂 configPath 注入
- pi-subagents 固定 vitest / coverage-istanbul 4.1.10,修复 npm ls 版本冲突
- 补 pi-web-search / pi-worktree UPSTREAM.md 与 pi-minimal-footer AGENTS.md,
  files 白名单补齐并加测试;.codegraph/.gitignore 移出跟踪
- detect_kitty 优先识别官方安装目录

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 23:18:34 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 56cbc83296
commit f9a7f743dd
46 changed files with 585 additions and 198 deletions
+66 -12
View File
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { existsSync } from "node:fs";
import { mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -142,19 +143,25 @@ test("template parser handles env names, braces and escapes", () => {
const AUTH_MJS = new URL("../scripts/search-auth.mjs", import.meta.url).pathname;
const CONFIGURE_SH = new URL("../configure.sh", import.meta.url).pathname;
function runMjs(args: string[], env: Record<string, string> = {}) {
return spawnSync("node", [AUTH_MJS, ...args], { encoding: "utf8", env: { ...process.env, ...env } });
function runMjs(args: string[], env: Record<string, string> = {}, input?: string) {
return spawnSync("node", [AUTH_MJS, ...args], {
encoding: "utf8",
env: { ...process.env, ...env },
...(input === undefined ? {} : { input }),
});
}
test("search-auth.mjs --set writes api_key entries and preserves other providers", async () => {
test("search-auth.mjs --set-stdin writes api_key entries and preserves other providers", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const authPath = join(dir, "auth.json");
await writeFile(
authPath,
JSON.stringify({ anthropic: { type: "oauth", access: "tok", refresh: "ref", expires: 42, accountId: "acc" } }),
);
const result = runMjs(["--auth", authPath, "--set", "tavily=tvly-new", "--set", "exa=!op read x"]);
assert.equal(result.status, 0, result.stderr);
const first = runMjs(["--auth", authPath, "--set-stdin", "tavily"], {}, "tvly-new\n");
assert.equal(first.status, 0, first.stderr);
const second = runMjs(["--auth", authPath, "--set-stdin", "exa"], {}, " !op read x \n");
assert.equal(second.status, 0, second.stderr);
const written = JSON.parse(await readFile(authPath, "utf8")) as Record<string, any>;
assert.deepEqual(written.anthropic, { type: "oauth", access: "tok", refresh: "ref", expires: 42, accountId: "acc" });
assert.deepEqual(written.tavily, { type: "api_key", key: "tvly-new" });
@@ -163,14 +170,33 @@ test("search-auth.mjs --set writes api_key entries and preserves other providers
assert.equal(getSearchApiKey("tavily", { authPath }), "tvly-new");
});
test("search-auth.mjs preserves an existing credential env map on --set", async () => {
test("search-auth.mjs --set is rejected: keys never travel in argv", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const authPath = join(dir, "auth.json");
const result = runMjs(["--auth", authPath, "--set", "tavily=tvly-leak"]);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /命令行参数|--set-stdin/);
assert.equal(existsSync(authPath), false);
});
test("search-auth.mjs allows only one --set-stdin and rejects empty stdin", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const authPath = join(dir, "auth.json");
const dup = runMjs(["--auth", authPath, "--set-stdin", "tavily", "--set-stdin", "exa"], {}, "key");
assert.notEqual(dup.status, 0);
const empty = runMjs(["--auth", authPath, "--set-stdin", "tavily"], {}, " \n");
assert.notEqual(empty.status, 0);
assert.equal(existsSync(authPath), false);
});
test("search-auth.mjs preserves an existing credential env map on --set-stdin", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const authPath = join(dir, "auth.json");
await writeFile(
authPath,
JSON.stringify({ keenable: { type: "api_key", key: "old", env: { KEENABLE_API_URL: "https://k.example" } } }),
);
const result = runMjs(["--auth", authPath, "--set", "keenable=new-key"]);
const result = runMjs(["--auth", authPath, "--set-stdin", "keenable"], {}, "new-key");
assert.equal(result.status, 0, result.stderr);
const written = JSON.parse(await readFile(authPath, "utf8")) as Record<string, any>;
assert.deepEqual(written.keenable, {
@@ -183,7 +209,7 @@ test("search-auth.mjs preserves an existing credential env map on --set", async
test("search-auth.mjs creates a missing auth.json and honors PI_CODING_AGENT_DIR", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const agentDir = join(dir, "agent");
const result = runMjs(["--set", "keenable=keen-1"], { PI_CODING_AGENT_DIR: agentDir });
const result = runMjs(["--set-stdin", "keenable"], { PI_CODING_AGENT_DIR: agentDir }, "keen-1");
assert.equal(result.status, 0, result.stderr);
const written = JSON.parse(await readFile(join(agentDir, "auth.json"), "utf8")) as Record<string, any>;
assert.deepEqual(written.keenable, { type: "api_key", key: "keen-1" });
@@ -204,7 +230,7 @@ test("search-auth.mjs refuses to clobber a malformed auth.json", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const authPath = join(dir, "auth.json");
await writeFile(authPath, "{oops");
const result = runMjs(["--auth", authPath, "--set", "tavily=x"]);
const result = runMjs(["--auth", authPath, "--set-stdin", "tavily"], {}, "x");
assert.notEqual(result.status, 0);
assert.match(result.stderr, /不是合法 JSON|abort|clobber/i);
assert.equal(await readFile(authPath, "utf8"), "{oops");
@@ -238,15 +264,43 @@ test("search-auth.mjs --has and --list never print key material", async () => {
assert.doesNotMatch(list.stdout, /supersecret/);
});
test("configure.sh search writes keys through search-auth.mjs noninteractively", async () => {
test("configure.sh search writes keys through search-auth.mjs via stdin", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const authPath = join(dir, "auth.json");
const result = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--tavily", "tvly-sh", "--exa", "exa-sh"], {
const first = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--tavily", "-"], {
encoding: "utf8",
input: "tvly-sh\n",
});
assert.equal(result.status, 0, result.stderr);
assert.equal(first.status, 0, first.stderr);
const second = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--exa", "-"], {
encoding: "utf8",
input: "exa-sh",
});
assert.equal(second.status, 0, second.stderr);
const written = JSON.parse(await readFile(authPath, "utf8")) as Record<string, any>;
assert.deepEqual(written.tavily, { type: "api_key", key: "tvly-sh" });
assert.deepEqual(written.exa, { type: "api_key", key: "exa-sh" });
assert.equal((await stat(authPath)).mode & 0o777, 0o600);
});
test("configure.sh search rejects a plaintext key argument", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const authPath = join(dir, "auth.json");
const result = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--tavily", "tvly-leak"], {
encoding: "utf8",
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /命令行参数/);
assert.equal(existsSync(authPath), false);
});
test("configure.sh search allows at most one stdin provider per call", async () => {
const dir = await mkdtemp(join(tmpdir(), "my-pi-auth-write-"));
const authPath = join(dir, "auth.json");
const result = spawnSync("sh", [CONFIGURE_SH, "search", "--auth", authPath, "--tavily", "-", "--exa", "-"], {
encoding: "utf8",
input: "one-key",
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /最多一个/);
});