mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:35:12 +01:00
fix(runner): discover assigned tools when direct catalogs exceed limits (#14218)
Keep assigned app tools accessible when the combined Runner catalog exceeds its operation or byte limits. Reserve task and completion tools, then expose bounded discovery and call tools for large catalogs. Fetch oversized schemas in reauthorized chunks without blocking later search results. Retain task ownership, work-mode restrictions, pinned assignments, current gateway authorization, approvals, and audit. Small catalogs stay direct. Validation: 46 focused server tests, two Runner capacity tests, local typecheck/build, 52 passing CI checks, and Greptile 5/5 with no open findings. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
640dee1802
commit
5ee9e751fb
9 files changed
+319
-12
No files matched your search
@@ -20,8 +20,10 @@ import { initializeRunIdentity, reserveSteeredIdentity, reconcileSteeredIdentity
|
||||
import { documentService } from "../documents.js";
|
||||
import { issueService } from "../issues.js";
|
||||
import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js";
|
||||
import { createAssignedMcpTools } from "./assigned-mcp-tools.js";
|
||||
import type { ToolGatewayService } from "../tool-gateway.js";
|
||||
import { READ_CURRENT_WAKE_COMMENTS_TOOL_NAME } from "./current-wake-comments.js";
|
||||
import { CAPABILITY_SEMANTIC_TOOL_CATALOG } from "../../vendor/paperclip-runner/index.js";
|
||||
import { CAPABILITY_SEMANTIC_TOOL_CATALOG, runnerCodexDynamicToolsFit } from "../../vendor/paperclip-runner/index.js";
|
||||
|
||||
describe("PaperclipRunnerToolAuthority", () => {
|
||||
let temporary: Awaited<
|
||||
@@ -610,6 +612,38 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
).resolves.toMatchObject({ approval: { id: approvalId }, tasks: [] });
|
||||
});
|
||||
|
||||
it("fits large assigned catalogs alongside workspace and completion tools without dropping task tools", async () => {
|
||||
const listToolsForNamedGateway = vi.fn().mockResolvedValue(Array.from({ length: 224 }, (_, i) => ({
|
||||
name: `app.action_${i}`, displayName: `Action ${i}`, description: "Read a fixture",
|
||||
parametersSchema: { type: "object", properties: {} }, risk: "read",
|
||||
})));
|
||||
const assignedMcpTools = await createAssignedMcpTools({
|
||||
gateway: { listToolsForNamedGateway } as unknown as ToolGatewayService,
|
||||
gatewayPublicId: "fixture", bearerToken: "fixture-token",
|
||||
});
|
||||
const binding = { companyId, agentId, issueId, runId, workspaceRoot: "/tmp/fixture-workspace" };
|
||||
const baseline = new PaperclipRunnerToolAuthority(db, binding).definitions();
|
||||
expect(runnerCodexDynamicToolsFit([...baseline, ...assignedMcpTools.definitions()])).toBe(false);
|
||||
const authority = new PaperclipRunnerToolAuthority(db, { ...binding, assignedMcpTools });
|
||||
const tools = authority.definitions();
|
||||
expect(runnerCodexDynamicToolsFit(tools)).toBe(true);
|
||||
expect(tools).toEqual(expect.arrayContaining(baseline));
|
||||
expect(tools.filter(tool => String(tool.name).startsWith("app_"))).toEqual([]);
|
||||
expect(tools.map(tool => tool.name)).toEqual(expect.arrayContaining([
|
||||
"paperclip_search_assigned_tools", "paperclip_call_assigned_tool", "register_deliverable",
|
||||
]));
|
||||
const call = { tool: "paperclip_search_assigned_tools", callId: "discover", arguments: { query: "Action 223" } };
|
||||
await expect(authority.execute(call)).resolves.toMatchObject({ tools: [expect.objectContaining({ description: "Action 223: Read a fixture" })] });
|
||||
listToolsForNamedGateway.mockClear();
|
||||
await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, runId));
|
||||
try {
|
||||
await expect(authority.execute(call)).rejects.toThrow("paperclip_runner_tool_binding_not_authorized");
|
||||
expect(listToolsForNamedGateway).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await db.update(heartbeatRuns).set({ status: "running" }).where(eq(heartbeatRuns.id, runId));
|
||||
}
|
||||
});
|
||||
|
||||
it("relays assigned MCP calls only while the native run still owns its task", async () => {
|
||||
const tool = { name: "app_mem0_recall", description: "Recall memory", inputSchema: { type: "object" } };
|
||||
const execute = vi.fn().mockResolvedValue({ content: "synthetic memory" });
|
||||
|
||||
Reference in new issue
Block a user