feat: enable permission-aware subagents

This commit is contained in:
叶林立
2026-08-26 10:49:26 +08:00
parent d3bf562189
commit 7571ba6dd9
185 changed files with 48365 additions and 26 deletions
@@ -0,0 +1,97 @@
/**
* mention-clone-tool-reachability.e2e.test.ts — reachability guard for the one
* tool the mention clone is built around.
*
* `runMentionClone` hands a session ONE tool and expects the model to call it.
* Whether that tool ever reaches the model is decided entirely inside Pi, by
* `createAgentSession`'s allowlist plumbing — and the unit tests cannot see it:
* their `createAgentSession` is a mock that hands `customTools[0]` straight to
* the model turn, so a session option that silently strips the tool passes
* every one of them.
*
* That is not hypothetical. The clone shipped with `noTools: "all"` on the
* reading its doc comment invites ("start with no tools enabled" — no
* built-ins, keep mine). Pi turns that flag into an EMPTY allowlist, and an
* empty array is truthy, so `AgentSession` builds an empty `Set` and
* `isAllowedTool` rejects every name — custom tools are filtered by the same
* predicate as built-ins. Every mention was prompted with no tools, answered in
* prose, and fell back to a direct start with a warning. The unit suite stayed
* green throughout.
*
* So this asserts against a REAL session, on the two things a mock cannot
* establish:
* 1. the clone's `Agent` tool is actually active on it, and
* 2. nothing else is — the invisible turn cannot read, write or run anything.
*
* No network/LLM: a faux provider satisfies session construction, and the
* assertion is on the constructed tool set rather than on a model turn.
*/
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Real pi-mono session construction; a cold first run under full-suite CPU
// contention can exceed vitest's 5s default.
vi.setConfig({ testTimeout: 30_000 });
// Hoisted so the (lifted) mock factory can reach it. Everything except the
// capture is the real module — the point is to construct a REAL session.
const { sessions } = vi.hoisted(() => ({ sessions: [] as any[] }));
vi.mock("@earendil-works/pi-coding-agent", async () => {
const actual = await vi.importActual<any>("@earendil-works/pi-coding-agent");
return {
...actual,
createAgentSession: async (opts: any) => {
const created = await actual.createAgentSession(opts);
sessions.push(created.session);
return created;
},
};
});
import { runMentionClone } from "../../src/mention-clone.js";
import { fauxModelBackend } from "../helpers/faux-model-backend.js";
import { registerFauxProvider } from "../helpers/pi-ai.js";
describe("mention clone tool reachability against real pi-mono", () => {
let cwd: string;
let faux: ReturnType<typeof registerFauxProvider>;
beforeEach(() => {
sessions.length = 0;
cwd = mkdtempSync(join(tmpdir(), "subagents-mention-clone-"));
faux = registerFauxProvider({ provider: "faux", models: [{ id: "faux-1", contextWindow: 200_000 }] });
});
afterEach(() => {
faux.unregister();
rmSync(cwd, { recursive: true, force: true });
});
it("the clone's Agent tool is live on the real session, and it is the only one", async () => {
const model = faux.getModel();
const backend = fauxModelBackend(model);
const ctx: any = {
cwd,
model,
getSystemPrompt: () => "PARENT",
// mention-clone reads the runtime off the registry facade, the same shim
// agent-runner carries for Pi >= 0.80.8.
modelRegistry: { ...backend.modelRegistry, runtime: backend.modelRuntime },
sessionManager: { getEntries: () => [], getLeafId: () => undefined },
};
// Never called: the assertion is on what the session exposes, not on the
// faux model deciding to use it.
const agentTool = { name: "Agent", execute: vi.fn() } as any;
// Never rejects by contract; a faux turn that cannot complete is fine,
// because the tool set is fixed at construction.
await runMentionClone({ ctx, type: "Explore", message: "go", agentTool });
expect(sessions).toHaveLength(1);
// The bug this file exists for: with an empty allowlist this is `[]`.
expect(sessions[0].getActiveToolNames()).toEqual(["Agent"]);
});
});