feat: allowlist xdev prompt docs

This commit is contained in:
Joe Shull
2026-07-23 12:06:57 -06:00
committed by can1357
parent f15191c37d
commit f4641c165e
9 changed files with 155 additions and 9 deletions
@@ -4058,6 +4058,18 @@ export const SETTINGS_SCHEMA = {
},
},
"tools.xdevInlineDevices": {
type: "array",
default: EMPTY_STRING_ARRAY,
ui: {
tab: "tools",
group: "Discovery & MCP",
label: "xd:// Inline Devices",
description:
"When xd:// Prompt Docs is Built-ins Only, inline dynamic devices whose names match these glob patterns (for example mcp__context_mode_*). Catalog Only ignores this setting.",
},
},
// MCP
"mcp.enableProjectConfig": {
type: "boolean",
@@ -437,6 +437,11 @@ export class SelectorController {
this.ctx.showError(`Failed to apply personality: ${err}`);
});
break;
case "tools.xdevDocs":
void this.ctx.session.refreshBaseSystemPrompt().catch(err => {
this.ctx.showError(`Failed to apply xd:// prompt docs setting: ${err}`);
});
break;
case "memory.backend":
void this.ctx.session.applyMemoryBackend().catch(err => {
this.ctx.showError(`Failed to apply memory backend: ${err}`);
@@ -13,4 +13,8 @@ No longer mounted (writes to these devices will fail):
- xd://{{this.name}}
{{/each}}
{{/if}}
{{#if docs}}
Configured inline device docs:
{{docs}}
{{/if}}
</system-notice>
+5 -1
View File
@@ -2596,7 +2596,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
cwd,
additionalWorkspaceRoots: sessionManager.getAdditionalDirectories(),
xdevTools: toolSession.xdevRegistry?.entries() ?? [],
xdevDocs: toolSession.xdevRegistry?.docsAll(settings.get("tools.xdevDocs")) ?? "",
xdevDocs:
toolSession.xdevRegistry?.docsAll(
settings.get("tools.xdevDocs"),
settings.get("tools.xdevInlineDevices"),
) ?? "",
autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings),
resolvedCustomPrompt: options.customSystemPrompt,
skills: session?.skills ?? skills,
@@ -566,10 +566,15 @@ export class SessionTools {
const summaries = new Map(this.#xdevRegistry?.entries().map(entry => [entry.name, entry.summary]) ?? []);
const added = [...pending.added].map(name => ({ name, summary: summaries.get(name) ?? "" }));
const removed = [...pending.removed].map(name => ({ name }));
const docs = this.#xdevRegistry?.docsFor(
pending.added,
this.#host.settings.get("tools.xdevDocs"),
this.#host.settings.get("tools.xdevInlineDevices"),
);
return {
role: "custom",
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
content: prompt.render(xdevMountNoticePrompt, { added, removed }),
content: prompt.render(xdevMountNoticePrompt, { added, removed, docs }),
attribution: "agent",
display: false,
timestamp: Date.now(),
+49 -5
View File
@@ -173,6 +173,16 @@ function toolSummary(inst: Tool): string {
return firstLine?.trim() ?? inst.label ?? inst.name;
}
function promptCatalogSummary(inst: Tool, maxLength?: number): string {
const summary =
toolSummary(inst)
.split("\n")
.find(line => line.trim().length > 0)
?.trim() ?? inst.name;
if (maxLength === undefined || summary.length <= maxLength) return summary;
return `${summary.slice(0, maxLength).trimEnd()}…`;
}
/** Decode the (possibly partially streamed) inner args JSON string into display args. */
function decodeInnerArgs(raw: unknown): Record<string, unknown> {
if (typeof raw !== "string" || raw.length === 0) return {};
@@ -233,7 +243,13 @@ export class XdevRegistry {
/** `{name, summary}` pairs for prompt templates and /tools display. */
entries(): Array<{ name: string; summary: string }> {
return this.list().map(tool => ({ name: tool.name, summary: toolSummary(tool) }));
return this.list().map(tool => ({
name: tool.name,
summary: promptCatalogSummary(
tool,
this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined,
),
}));
}
/** `read xd://` listing with one device per line. */
@@ -276,13 +292,12 @@ export class XdevRegistry {
* Dynamic mounts embed at most {@link EXTERNAL_DESCRIPTION_CAP} description
* chars (schema always intact); `read xd://<tool>` returns the full text.
*/
docsAll(mode: XdevDocsMode = "inline"): string {
docsAll(mode: XdevDocsMode = "inline", inlinePatterns: readonly string[] = []): string {
const sections: string[] = [];
const overflow: Tool[] = [];
let used = 0;
for (const tool of this.list()) {
const isBuiltin = this.#builtins.has(tool.name);
if (mode === "catalog" || (mode === "builtins" && !isBuiltin)) {
if (!this.#shouldInline(tool, mode, inlinePatterns)) {
overflow.push(tool);
continue;
}
@@ -299,7 +314,10 @@ export class XdevRegistry {
sections.push(
[
"## Additional devices (docs on demand)",
...overflow.map(tool => `- ${XD_URL_PREFIX}${tool.name} — ${toolSummary(tool)}`),
...overflow.map(tool => {
const maxLength = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined;
return `- ${XD_URL_PREFIX}${tool.name} — ${promptCatalogSummary(tool, maxLength)}`;
}),
"",
`Read ${XD_URL_PREFIX}<tool> for full docs + JSON schema before first use.`,
].join("\n"),
@@ -308,6 +326,32 @@ export class XdevRegistry {
return sections.join("\n\n");
}
/** Docs for selected mounted devices under the configured prompt-doc policy. */
docsFor(names: Iterable<string>, mode: XdevDocsMode, inlinePatterns: readonly string[] = []): string {
const sections: string[] = [];
let used = 0;
for (const name of names) {
const tool = this.get(name);
if (!tool || !this.#shouldInline(tool, mode, inlinePatterns)) continue;
const descriptionCap = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined;
const docs = renderDocs(tool, "##", descriptionCap);
if (docs.length > XdevRegistry.DOCS_PER_DEVICE_CAP || used + docs.length > XdevRegistry.DOCS_TOTAL_BUDGET)
continue;
used += docs.length;
sections.push(docs);
}
return sections.join("\n\n");
}
#shouldInline(tool: Tool, mode: XdevDocsMode, inlinePatterns: readonly string[]): boolean {
return (
mode !== "catalog" &&
(mode === "inline" ||
this.#builtins.has(tool.name) ||
inlinePatterns.some(pattern => new Bun.Glob(pattern).match(tool.name)))
);
}
#resolve(name: string): Tool {
const inst = this.get(name);
if (!inst) {
@@ -601,6 +601,41 @@ describe("AgentSession refreshMCPTools rebuild skipping", () => {
expect(allNotices[1]).not.toContain("became available");
});
it("caps dynamic xd:// mount-notice summaries", async () => {
const { session, contexts } = newSession(async toolNames => `tools:${toolNames.join(",")}`, {
xdevRegistry: new XdevRegistry([]),
responses: [{ content: ["ok"] }],
});
const description = `Search ${"x".repeat(XdevRegistry.EXTERNAL_DESCRIPTION_CAP * 3)} TAIL`;
const search = createMcpCustomTool("mcp__nucleus_search", "nucleus", "search", description);
await session.refreshMCPTools([search]);
await session.prompt("hello");
const notices = mountNoticesIn(contexts[0]);
expect(notices).toHaveLength(1);
expect(notices[0]).toContain("xd://mcp__nucleus_search");
expect(notices[0]).not.toContain("TAIL");
});
it("inlines configured late xd:// device docs in mount notices", async () => {
const { session, contexts } = newSession(async toolNames => `tools:${toolNames.join(",")}`, {
xdevRegistry: new XdevRegistry([]),
responses: [{ content: ["ok"] }],
});
session.settings.set("tools.xdevDocs", "builtins");
session.settings.set("tools.xdevInlineDevices", ["mcp__nucleus_*"]);
const search = createMcpCustomTool("mcp__nucleus_search", "nucleus", "search", "Search nucleus");
await session.refreshMCPTools([search]);
await session.prompt("hello");
const notices = mountNoticesIn(contexts[0]);
expect(notices).toHaveLength(1);
expect(notices[0]).toContain("## mcp__nucleus_search");
expect(notices[0]).toContain("## Schema");
});
it("drops a mount delta that cancels out before the next prompt", async () => {
const { session, contexts } = newSession(async toolNames => `tools:${toolNames.join(",")}`, {
xdevRegistry: new XdevRegistry([]),
@@ -0,0 +1,20 @@
import { describe, expect, it, vi } from "bun:test";
import { SelectorController } from "@oh-my-pi/pi-coding-agent/modes/controllers/selector-controller";
import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types";
describe("SelectorController prompt-affecting settings", () => {
it("refreshes the active prompt when xdev docs mode changes", async () => {
const refreshBaseSystemPrompt = vi.fn(async () => {});
const ctx = {
session: { refreshBaseSystemPrompt },
showError: vi.fn(),
} as unknown as InteractiveModeContext;
const controller = new SelectorController(ctx);
controller.handleSettingChange("tools.xdevDocs", "catalog");
await Promise.resolve();
expect(refreshBaseSystemPrompt).toHaveBeenCalledTimes(1);
expect(ctx.showError).not.toHaveBeenCalled();
});
});
@@ -219,10 +219,13 @@ describe("read and write route xd:// device URLs", () => {
if (!registry) throw new Error("expected xdev registry");
const mounted = registry.list();
const longDescription = "LEDE " + "y".repeat(XdevRegistry.EXTERNAL_DESCRIPTION_CAP * 3) + " TAIL";
const longDescription = `LEDE ${"y".repeat(XdevRegistry.EXTERNAL_DESCRIPTION_CAP * 3)} TAIL`;
const external = Object.create(mounted[0]!) as (typeof mounted)[number];
Object.defineProperty(external, "name", { value: "mcp_external_tool" });
Object.defineProperty(external, "description", { value: longDescription });
Object.defineProperty(external, "summary", {
value: `SUMMARY ${"z".repeat(XdevRegistry.EXTERNAL_DESCRIPTION_CAP * 3)} TAIL`,
});
registry.reconcile([external]);
const inlineDocs = registry.docsAll("inline");
@@ -235,12 +238,26 @@ describe("read and write route xd:// device URLs", () => {
expect(builtinsDocs).toContain("## ");
expect(builtinsDocs).not.toContain("## mcp_external_tool");
expect(builtinsDocs).toContain("- xd://mcp_external_tool —");
expect(builtinsDocs).not.toContain("TAIL");
const catalogDocs = registry.docsAll("catalog");
expect(catalogDocs).not.toContain(`## ${mounted[0]!.name}`);
expect(catalogDocs).toContain("- xd://");
expect(catalogDocs).toContain("- xd://mcp_external_tool —");
expect(registry.docs("mcp_external_tool")).toContain("TAIL");
const contextMode = Object.create(mounted[0]!) as (typeof mounted)[number];
Object.defineProperty(contextMode, "name", { value: "mcp__context_mode_ctx_execute" });
const unrelatedMcp = Object.create(mounted[0]!) as (typeof mounted)[number];
Object.defineProperty(unrelatedMcp, "name", { value: "mcp__other_server_execute" });
registry.reconcile([contextMode, unrelatedMcp]);
const allowlistedDocs = registry.docsAll("builtins", ["mcp__context_mode_*"]);
expect(allowlistedDocs).toContain("## mcp__context_mode_ctx_execute");
expect(allowlistedDocs).not.toContain("## mcp__other_server_execute");
expect(allowlistedDocs).toContain("- xd://mcp__other_server_execute —");
const catalogWithAllowlistDocs = registry.docsAll("catalog", ["mcp__context_mode_*"]);
expect(catalogWithAllowlistDocs).not.toContain("## mcp__context_mode_ctx_execute");
} finally {
await removeWithRetries(tempDir);
}