mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-07 07:23:08 +02: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
@@ -15,6 +15,123 @@ function fixture(tools: ToolGatewayDescriptor[]) {
|
||||
}
|
||||
|
||||
describe("assigned MCP runner tools", () => {
|
||||
const searchName = "paperclip_search_assigned_tools";
|
||||
const callName = "paperclip_call_assigned_tool";
|
||||
|
||||
it("pages through an oversized catalog and calls every tool through the original gateway", async () => {
|
||||
const f = fixture(Array.from({ length: 224 }, (_, i) => descriptor(`app.action_${i}`)));
|
||||
const assigned = await createAssignedMcpTools(f);
|
||||
const direct = assigned.definitions();
|
||||
const compact = assigned.definitions(tools => tools.length <= 200);
|
||||
expect(compact.map(tool => tool.name)).toEqual([searchName, callName]);
|
||||
expect(assigned.definitions(tools => tools.length <= 224)).toEqual(direct);
|
||||
expect(() => assigned.definitions(() => false)).toThrow("assigned_mcp_tool_catalog_capacity_exceeded");
|
||||
const seen: string[] = [];
|
||||
let offset: number | null = 0;
|
||||
do {
|
||||
const page = await assigned.execute({ tool: searchName, arguments: { query: "", offset, limit: 20 } }) as {
|
||||
tools: Array<{ name: string; inputSchema: unknown }>; nextOffset: number | null;
|
||||
};
|
||||
expect(page.tools.length).toBeLessThanOrEqual(20);
|
||||
for (const tool of page.tools) {
|
||||
seen.push(tool.name);
|
||||
expect(tool.inputSchema).toEqual(descriptor("any").parametersSchema);
|
||||
await assigned.execute({ tool: callName, arguments: { name: tool.name, arguments: { query: "fixture" } } });
|
||||
}
|
||||
offset = page.nextOffset;
|
||||
} while (offset !== null);
|
||||
expect(seen).toEqual(direct.map(tool => tool.name).sort());
|
||||
expect(f.executeTool).toHaveBeenCalledTimes(224);
|
||||
expect(new Set(f.executeTool.mock.calls.map(([call]) => call.tool)).size).toBe(224);
|
||||
for (const [call] of f.executeTool.mock.calls) {
|
||||
expect(call).toMatchObject({ gatewayPublicId: f.gatewayPublicId, sessionToken: f.bearerToken, parameters: { query: "fixture" } });
|
||||
expect(call).not.toHaveProperty("approvedActionRequestId");
|
||||
}
|
||||
});
|
||||
|
||||
it("searches only pinned tools still granted by fresh discovery without projecting metadata", async () => {
|
||||
const f = fixture([descriptor("calendar.search"), descriptor("mail.search"), descriptor("calendar.remove", "write")]);
|
||||
const assigned = await createAssignedMcpTools(f);
|
||||
f.listToolsForNamedGateway.mockResolvedValue([
|
||||
{ ...descriptor("calendar.search"), providerMetadata: { token: "secret-provider-token" } },
|
||||
descriptor("calendar.remove", "write"), descriptor("calendar.new_grant"),
|
||||
]);
|
||||
const result = await assigned.execute({ tool: searchName, arguments: { query: "calendar" } }, "planning");
|
||||
expect(result).toEqual({ tools: [assigned.definitions()[0]], nextOffset: null });
|
||||
expect(JSON.stringify(result)).not.toMatch(/private-|secret-provider|gateway-fixture|new_grant/);
|
||||
f.listToolsForNamedGateway.mockRejectedValue(new Error("gateway_token_revoked"));
|
||||
await expect(assigned.execute({ tool: searchName, arguments: { query: "" } })).rejects.toThrow("gateway_token_revoked");
|
||||
});
|
||||
|
||||
it.each(["planning", "ask"] as const)("preserves pinned and fresh %s restrictions through the call wrapper", async mode => {
|
||||
const f = fixture([descriptor("read"), descriptor("write", "write")]);
|
||||
const assigned = await createAssignedMcpTools(f);
|
||||
const restricted = await createAssignedMcpTools({ ...f, workMode: mode });
|
||||
const call = { tool: callName, arguments: { name: assigned.definitions()[1]!.name, arguments: {} } };
|
||||
await expect(assigned.execute(call, mode)).rejects.toThrow("paperclip_runner_tool_mode_denied");
|
||||
await expect(restricted.execute(call, "standard")).rejects.toThrow("paperclip_runner_tool_mode_denied");
|
||||
expect(f.executeTool).not.toHaveBeenCalled();
|
||||
const discovery = await restricted.execute({ tool: searchName, arguments: { query: "" } }, "standard");
|
||||
expect(discovery).toEqual({ tools: [assigned.definitions()[0]], nextOffset: null });
|
||||
});
|
||||
|
||||
it.each(["approval_required", "connection_revoked", "tool_error"])("preserves %s through the call wrapper", async reason => {
|
||||
const f = fixture([descriptor("write", "write")]);
|
||||
const assigned = await createAssignedMcpTools(f);
|
||||
const error = new ToolGatewayHttpError(403, "Denied", reason);
|
||||
f.executeTool.mockRejectedValue(error);
|
||||
await expect(assigned.execute({ tool: callName, arguments: { name: assigned.definitions()[0]!.name, arguments: {} } })).rejects.toBe(error);
|
||||
});
|
||||
|
||||
it("bounds search pages by bytes and rejects invalid wrapper arguments and unassigned targets", async () => {
|
||||
const f = fixture(Array.from({ length: 4 }, (_, i) => ({
|
||||
...descriptor(`large_${i}`),
|
||||
parametersSchema: { type: "object", description: "x".repeat(250 * 1024) },
|
||||
})));
|
||||
const assigned = await createAssignedMcpTools(f);
|
||||
const page = await assigned.execute({ tool: searchName, arguments: { query: "", limit: 20 } }) as { tools: unknown[]; nextOffset: number };
|
||||
expect(page.tools).toHaveLength(2);
|
||||
expect(page.nextOffset).toBe(2);
|
||||
for (const args of [null, [], { query: "x".repeat(201) }, { query: "", offset: -1 }, { query: "", limit: 21 }, { query: "", offset: 0.5 }]) {
|
||||
await expect(assigned.execute({ tool: searchName, arguments: args })).rejects.toThrow("assigned_mcp_tool_invalid_arguments");
|
||||
}
|
||||
for (const name of ["unassigned", searchName, callName]) {
|
||||
await expect(assigned.execute({ tool: callName, arguments: { name, arguments: {} } })).rejects.toThrow("assigned_mcp_tool_unknown");
|
||||
}
|
||||
await expect(assigned.execute({ tool: callName, arguments: { name: "unassigned", arguments: [] } })).rejects.toThrow("assigned_mcp_tool_invalid_arguments");
|
||||
expect(f.executeTool).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps individually oversized schemas and later tools discoverable with bounded schema chunks", async () => {
|
||||
const schema = { type: "object", description: "\u0000🙂".repeat(150_000), properties: {} };
|
||||
const f = fixture([{ ...descriptor("a_large"), parametersSchema: schema }, descriptor("z_small")]);
|
||||
const assigned = await createAssignedMcpTools(f);
|
||||
const large = assigned.definitions()[0]!.name as string;
|
||||
const page = await assigned.execute({ tool: searchName, arguments: { query: "", limit: 1 } }) as {
|
||||
tools: Array<Record<string, unknown>>; nextOffset: number;
|
||||
};
|
||||
expect(page.tools).toEqual([{ name: large, description: "a_large: Use a_large", inputSchemaRef: large }]);
|
||||
expect(page.nextOffset).toBe(1);
|
||||
await expect(assigned.execute({ tool: searchName, arguments: { query: "", offset: page.nextOffset } }))
|
||||
.resolves.toEqual({ tools: [assigned.definitions()[1]], nextOffset: null });
|
||||
let schemaOffset: number | null = 0;
|
||||
let serialized = "";
|
||||
do {
|
||||
const chunk = await assigned.execute({ tool: searchName, arguments: { query: "", schemaTool: large, schemaOffset } }) as {
|
||||
schemaJson: string; nextSchemaOffset: number | null;
|
||||
};
|
||||
expect(Buffer.byteLength(JSON.stringify(chunk))).toBeLessThan(640 * 1024);
|
||||
serialized += chunk.schemaJson;
|
||||
schemaOffset = chunk.nextSchemaOffset;
|
||||
} while (schemaOffset !== null);
|
||||
expect(JSON.parse(serialized)).toEqual(schema);
|
||||
await assigned.execute({ tool: callName, arguments: { name: large, arguments: {} } });
|
||||
expect(f.executeTool).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ tool: "a_large" }));
|
||||
f.listToolsForNamedGateway.mockResolvedValue([descriptor("z_small")]);
|
||||
await expect(assigned.execute({ tool: searchName, arguments: { query: "", schemaTool: large } })).rejects.toThrow("assigned_mcp_tool_unknown");
|
||||
await expect(assigned.execute({ tool: searchName, arguments: { query: "", schemaOffset: 1 } })).rejects.toThrow("assigned_mcp_tool_invalid_arguments");
|
||||
});
|
||||
|
||||
it("requires a configured gateway registered for the exact database instance", () => {
|
||||
const firstDb = {} as Db;
|
||||
const secondDb = {} as Db;
|
||||
|
||||
@@ -3,6 +3,51 @@ import type { Db } from "@paperclipai/db";
|
||||
import { ToolGatewayHttpError, type ToolGatewayDescriptor, type ToolGatewayService } from "../tool-gateway.js";
|
||||
|
||||
type WorkMode = "standard" | "planning" | "ask";
|
||||
type ToolDefinition = Record<string, unknown>;
|
||||
|
||||
const SEARCH_TOOL = "paperclip_search_assigned_tools";
|
||||
const CALL_TOOL = "paperclip_call_assigned_tool";
|
||||
// A single valid runner schema can be 512 KiB. Leave room for its description
|
||||
// while keeping the complete result below the 768 KiB provider-result bound.
|
||||
const SEARCH_PAGE_BYTES = 640 * 1024;
|
||||
const SCHEMA_CHUNK_CHARACTERS = 64 * 1024;
|
||||
const ON_DEMAND_TOOLS: ToolDefinition[] = [
|
||||
{
|
||||
name: SEARCH_TOOL,
|
||||
description: "Search your assigned app tools by name or description and read their input schemas. Use an empty query to browse. Pass nextOffset to fetch the next page. A large definition returns inputSchemaRef: set schemaTool to that name with an empty query, then pass nextSchemaOffset until null. Concatenate schemaJson chunks and parse JSON. Use paperclip_call_assigned_tool with a returned name and arguments matching its inputSchema.",
|
||||
inputSchema: {
|
||||
type: "object", additionalProperties: false,
|
||||
properties: {
|
||||
query: { type: "string", maxLength: 200 },
|
||||
offset: { type: "integer", minimum: 0 },
|
||||
limit: { type: "integer", minimum: 1, maximum: 20 },
|
||||
schemaTool: { type: "string", description: "Exact inputSchemaRef from a search result." },
|
||||
schemaOffset: { type: "integer", minimum: 0 },
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: CALL_TOOL,
|
||||
description: "Call an assigned app tool discovered with paperclip_search_assigned_tools. Use its exact returned name and arguments matching its inputSchema. The same permissions, approvals, and audit rules apply as for direct app tools.",
|
||||
inputSchema: {
|
||||
type: "object", additionalProperties: false,
|
||||
properties: { name: { type: "string" }, arguments: { type: "object", additionalProperties: true } },
|
||||
required: ["name", "arguments"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error("assigned_mcp_tool_invalid_arguments");
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function definition(name: string, tool: ToolGatewayDescriptor): ToolDefinition {
|
||||
return { name, description: `${tool.displayName}: ${tool.description}`, inputSchema: structuredClone(tool.parametersSchema) };
|
||||
}
|
||||
|
||||
// Execution must use the app's configured gateway, including deployment
|
||||
// restrictions, OAuth refresh, and approval delivery. Never fall back to an
|
||||
@@ -48,19 +93,82 @@ export async function createAssignedMcpTools(input: {
|
||||
}
|
||||
const permits = (tool: ToolGatewayDescriptor, mode: WorkMode = input.workMode ?? "standard") => mode === "standard" || tool.risk === "read";
|
||||
|
||||
async function search(argumentsValue: unknown, currentWorkMode?: WorkMode) {
|
||||
const args = object(argumentsValue);
|
||||
const offset = args.offset ?? 0;
|
||||
const limit = args.limit ?? 5;
|
||||
const schemaOffset = args.schemaOffset ?? 0;
|
||||
if (typeof args.query !== "string" || args.query.length > 200 ||
|
||||
typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0 ||
|
||||
typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1 || limit > 20 ||
|
||||
(args.schemaTool !== undefined && typeof args.schemaTool !== "string") ||
|
||||
typeof schemaOffset !== "number" || !Number.isSafeInteger(schemaOffset) || schemaOffset < 0 ||
|
||||
(args.schemaOffset !== undefined && args.schemaTool === undefined)) {
|
||||
throw new Error("assigned_mcp_tool_invalid_arguments");
|
||||
}
|
||||
// Intersect fresh grants with this session's pinned catalog. Revocations
|
||||
// take effect immediately; newly assigned tools require a new session.
|
||||
const current = new Map((await input.gateway.listToolsForNamedGateway({
|
||||
gatewayPublicId: input.gatewayPublicId, bearerToken: input.bearerToken,
|
||||
})).map(tool => [tool.name, tool]));
|
||||
const terms = args.query.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
||||
const authorized = [...tools].filter(([, tool]) => {
|
||||
const fresh = current.get(tool.name);
|
||||
return fresh && permits(tool) && permits(tool, currentWorkMode) &&
|
||||
permits(fresh) && permits(fresh, currentWorkMode);
|
||||
});
|
||||
if (typeof args.schemaTool === "string") {
|
||||
const selected = authorized.find(([name]) => name === args.schemaTool);
|
||||
if (!selected) throw new Error("assigned_mcp_tool_unknown");
|
||||
const schema = JSON.stringify(selected[1].parametersSchema);
|
||||
const end = schemaOffset + SCHEMA_CHUNK_CHARACTERS;
|
||||
return {
|
||||
name: selected[0], schemaJson: schema.slice(schemaOffset, end),
|
||||
nextSchemaOffset: end < schema.length ? end : null,
|
||||
};
|
||||
}
|
||||
const matches = authorized.filter(([name, tool]) =>
|
||||
terms.every(term => `${name} ${tool.displayName} ${tool.description}`.toLowerCase().includes(term)))
|
||||
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
||||
const page: ToolDefinition[] = [];
|
||||
for (const [name, tool] of matches.slice(offset, offset + limit)) {
|
||||
let next = definition(name, tool);
|
||||
if (Buffer.byteLength(JSON.stringify([next]), "utf8") > SEARCH_PAGE_BYTES) {
|
||||
// One large schema must not block browsing the tools after it. Expose
|
||||
// a reference whose schema can be fetched in bounded, reauthorized
|
||||
// chunks, without weakening the gateway's argument validation.
|
||||
next = { name, description: String(next.description).slice(0, 2000), inputSchemaRef: name };
|
||||
}
|
||||
if (Buffer.byteLength(JSON.stringify([...page, next]), "utf8") > SEARCH_PAGE_BYTES) {
|
||||
break;
|
||||
}
|
||||
page.push(next);
|
||||
}
|
||||
return { tools: page, nextOffset: offset + page.length < matches.length ? offset + page.length : null };
|
||||
}
|
||||
|
||||
return {
|
||||
definitions(): Array<Record<string, unknown>> {
|
||||
return [...tools].filter(([, tool]) => permits(tool)).map(([name, tool]) => ({
|
||||
name,
|
||||
description: `${tool.displayName}: ${tool.description}`,
|
||||
inputSchema: structuredClone(tool.parametersSchema),
|
||||
}));
|
||||
definitions(fits?: (definitions: ToolDefinition[]) => boolean): ToolDefinition[] {
|
||||
const direct = [...tools].filter(([, tool]) => permits(tool)).map(([name, tool]) => definition(name, tool));
|
||||
if (!fits || fits(direct)) return direct;
|
||||
const compact = structuredClone(ON_DEMAND_TOOLS);
|
||||
if (!fits(compact)) throw new Error("assigned_mcp_tool_catalog_capacity_exceeded");
|
||||
return compact;
|
||||
},
|
||||
has(name: string): boolean {
|
||||
return tools.has(name);
|
||||
return tools.has(name) || name === SEARCH_TOOL || name === CALL_TOOL;
|
||||
},
|
||||
async execute(call: { tool: string; arguments: unknown }, currentWorkMode?: WorkMode): Promise<unknown> {
|
||||
const descriptor = tools.get(call.tool);
|
||||
if (call.tool === SEARCH_TOOL) return search(call.arguments, currentWorkMode);
|
||||
let name = call.tool;
|
||||
let parameters = call.arguments;
|
||||
if (name === CALL_TOOL) {
|
||||
const args = object(parameters);
|
||||
if (typeof args.name !== "string") throw new Error("assigned_mcp_tool_invalid_arguments");
|
||||
name = args.name;
|
||||
parameters = object(args.arguments);
|
||||
}
|
||||
const descriptor = tools.get(name);
|
||||
if (!descriptor) throw new Error("assigned_mcp_tool_unknown");
|
||||
if (!permits(descriptor) || !permits(descriptor, currentWorkMode)) throw new Error("paperclip_runner_tool_mode_denied");
|
||||
// Reauthorize through the existing gateway on every call. Discovery is
|
||||
@@ -69,7 +177,7 @@ export async function createAssignedMcpTools(input: {
|
||||
gatewayPublicId: input.gatewayPublicId,
|
||||
sessionToken: input.bearerToken,
|
||||
tool: descriptor.name,
|
||||
parameters: call.arguments,
|
||||
parameters,
|
||||
});
|
||||
if (result.status !== "completed" && result.status !== "replayed") {
|
||||
throw new Error("assigned_mcp_tool_execution_incomplete");
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
issues,
|
||||
issueThreadInteractions,
|
||||
} from "@paperclipai/db";
|
||||
import { CAPABILITY_SEMANTIC_TOOL_CATALOG } from "../../vendor/paperclip-runner/index.js";
|
||||
import { CAPABILITY_SEMANTIC_TOOL_CATALOG, runnerCodexDynamicToolsFit } from "../../vendor/paperclip-runner/index.js";
|
||||
import { agentService } from "../agents.js";
|
||||
import { approvalService } from "../approvals.js";
|
||||
import { documentService } from "../documents.js";
|
||||
@@ -225,7 +225,11 @@ export class PaperclipRunnerToolAuthority {
|
||||
definitions.push(LIST_CHAT_ATTACHMENTS_TOOL_DEFINITION);
|
||||
definitions.push(REUSE_CHAT_ATTACHMENT_TOOL_DEFINITION);
|
||||
definitions.push(READ_CHAT_ATTACHMENT_TOOL_DEFINITION);
|
||||
return [...RUNTIME_CONNECTION_TOOL_DEFINITIONS, ...(this.binding.connectorAssignments ?? []).flatMap((assignment) => assignment.tools), ...(this.binding.assignedMcpTools?.definitions() ?? []), ...definitions];
|
||||
const connectionTools = [...RUNTIME_CONNECTION_TOOL_DEFINITIONS,
|
||||
...(this.binding.connectorAssignments ?? []).flatMap((assignment) => assignment.tools)];
|
||||
const assignedTools = this.binding.assignedMcpTools?.definitions((tools) =>
|
||||
runnerCodexDynamicToolsFit([...connectionTools, ...tools, ...definitions])) ?? [];
|
||||
return [...connectionTools, ...assignedTools, ...definitions];
|
||||
}
|
||||
|
||||
async execute(call: {
|
||||
|
||||
+1
@@ -68,6 +68,7 @@ const sourceUrl = new URL(
|
||||
const runner = (await import(sourceUrl.href)) as RunnerModule;
|
||||
|
||||
export const DurablePrpControlPlane = runner.DurablePrpControlPlane;
|
||||
export const runnerCodexDynamicToolsFit = runner.runnerCodexDynamicToolsFit;
|
||||
export const inspectWarmRunTransition = runner.inspectWarmRunTransition;
|
||||
export const readRunnerdArtifactBinding = runner.readRunnerdArtifactBinding;
|
||||
export const NativeSessionCleanupQuarantinedError =
|
||||
|
||||
Reference in new issue
Block a user