fix: restore pi rust builtin tool surface

- expose Pi Rust built-in tools by permission mode instead of replacing them with MNote file tools
- update Pi Lab smoke coverage for native ls/read/bash usage
- document the overreplacement regression and verification evidence
This commit is contained in:
Agent Board
2026-07-11 20:34:21 +08:00
parent 896c94b696
commit d47f6447fd
5 changed files with 3063 additions and 146 deletions
+22 -9
View File
@@ -80,7 +80,9 @@ async function seedWorkspaceAccess(rootUri, rootPath) {
}),
});
if (seed.status === 403 && seed.body && seed.body.code === "dev_seed_disabled") {
return { ok: false, skipped: true, reason: "dev_seed_disabled" };
throw new Error(
"Pi RPC smoke 需要 /api/dev/seed;请使用 `npm run dev:hot` 启动并重试。dev:hot 默认开启 MNOTE_WEB_ALLOW_DEV_FIXTURES=1,修改启动环境后必须重启 Node 主进程。",
);
}
assert(seed.status === 200 && seed.body.ok === true, `/api/dev/seed failed: ${seed.status} ${JSON.stringify(seed.body)}`);
return { ok: true, skipped: false };
@@ -215,7 +217,7 @@ async function main() {
assert(status.body.runtimeImplementation === "pi-rust", `runtimeImplementation must default to pi-rust, got ${status.body.runtimeImplementation}`);
assert(status.body.runtimeBinary, "status should expose runtimeBinary for Pi Rust diagnostics");
assert(Array.isArray(status.body.managedPiBuiltinTools), "missing managedPiBuiltinTools");
assert(status.body.managedPiBuiltinTools.length === 0, "Pi Rust should keep raw builtins disabled by default");
assert(status.body.managedPiBuiltinTools.length === 0, "status without an active full_access session should not claim raw builtins are enabled");
assert(status.body.uiMode === "independent_mnote_native_drawer", "Pi Lab should report independent native drawer UI mode");
pass("status enabled/rpc with Rust bridge policy and independent Pi Lab UI mode");
} catch (err) {
@@ -245,7 +247,11 @@ async function main() {
assert(start.body.session.runtimePid > 0, "runtimePid must be positive");
assert(start.body.session.allowedRootsSnapshot, "start did not capture allowed roots snapshot");
assert(Array.isArray(start.body.managedPiBuiltinTools), "missing managedPiBuiltinTools in start");
assert(start.body.managedPiBuiltinTools.length === 0, "start should not expose raw Pi builtins by default");
assert.deepEqual(
[...start.body.managedPiBuiltinTools].sort(),
["edit", "find", "grep", "hashline_edit", "ls", "read", "write"].sort(),
"auto_edit should expose Pi Rust read/write/edit builtins but keep bash for full_access",
);
assert(start.body.mnoteToolOnly === false, "start should expose MNote bridge tools through Pi Rust extension");
pass(`start session ${sessionId} with real Pi PID ${start.body.session.runtimePid}`);
} catch (err) {
@@ -288,7 +294,11 @@ async function main() {
assert(payload.mode === "rpc", `mode must be rpc, got ${payload.mode}`);
assert(typeof payload.pid === "number", "PID must be a number in event");
assert(Array.isArray(payload.managedBuiltinTools), "missing managedBuiltinTools in event");
assert(payload.managedBuiltinTools.length === 0, "runtime event should keep raw Pi builtins disabled by default");
assert.deepEqual(
[...payload.managedBuiltinTools].sort(),
["edit", "find", "grep", "hashline_edit", "ls", "read", "write"].sort(),
"runtime event should expose auto_edit Pi Rust builtins",
);
pass(`runtime_started event observed (mode=rpc, pid=${payload.pid})`);
} else {
const statusAfterStart = await fetchJson(`${BASE}/api/page-ai/pi/status`);
@@ -490,15 +500,18 @@ async function main() {
fail(`abort: ${err.message}`);
}
// ── 14. Verify session status changed to Aborted ───────────────
// ── 14. Verify aborted session leaves active status and persists in history
try {
const status2 = await fetchJson(`${BASE}/api/page-ai/pi/status`);
assert(status2.body.session, "status should return current session");
assert(status2.status === 200, `status returned ${status2.status}`);
assert(status2.body.session == null, "aborted session should not remain auto-resumable");
const history = await fetchJson(`${BASE}/api/page-ai/pi/sessions/${encodeURIComponent(sessionId)}`);
assert(history.status === 200, `session history returned ${history.status}`);
assert(
status2.body.session.status === "aborted",
`expected session status aborted, got ${status2.body.session.status}`
history.body.session?.status === "aborted",
`expected persisted session status aborted, got ${history.body.session?.status}`
);
pass("session status transitions to aborted");
pass("aborted session leaves active status and persists as aborted");
} catch (err) {
fail(`session status aborted: ${err.message}`);
}