Unified · Split
| @@ -280,7 +280,7 @@ export declare const api: { · 279 unmodified lines | ||
| 280 | 280 | bootstrapAnonymousSession: FunctionReference< |
| 281 | 281 | "mutation", |
| 282 | 282 | "public", |
| 283 | − { sessionId?: string }, | |
| 283 | + { actorId?: string; sessionId?: string }, | |
| 284 | 284 | any |
| 285 | 285 | >; |
| 286 | 286 | deleteToolSource: FunctionReference< |
| @@ -1219,6 +1219,115 @@ export declare const internal: { · 932 unmodified lines | ||
| 1219 | 1219 | any |
| 1220 | 1220 | >; |
| 1221 | 1221 | }; |
| 1222 | + toolRegistry: { | |
| 1223 | + beginBuild: FunctionReference< | |
| 1224 | + "mutation", | |
| 1225 | + "internal", | |
| 1226 | + { buildId: string; signature: string; workspaceId: Id<"workspaces"> }, | |
| 1227 | + any | |
| 1228 | + >; | |
| 1229 | + finishBuild: FunctionReference< | |
| 1230 | + "mutation", | |
| 1231 | + "internal", | |
| 1232 | + { buildId: string; workspaceId: Id<"workspaces"> }, | |
| 1233 | + any | |
| 1234 | + >; | |
| 1235 | + getState: FunctionReference< | |
| 1236 | + "query", | |
| 1237 | + "internal", | |
| 1238 | + { workspaceId: Id<"workspaces"> }, | |
| 1239 | + any | |
| 1240 | + >; | |
| 1241 | + getToolByPath: FunctionReference< | |
| 1242 | + "query", | |
| 1243 | + "internal", | |
| 1244 | + { buildId: string; path: string; workspaceId: Id<"workspaces"> }, | |
| 1245 | + any | |
| 1246 | + >; | |
| 1247 | + getToolsByNormalizedPath: FunctionReference< | |
| 1248 | + "query", | |
| 1249 | + "internal", | |
| 1250 | + { | |
| 1251 | + buildId: string; | |
| 1252 | + limit: number; | |
| 1253 | + normalizedPath: string; | |
| 1254 | + workspaceId: Id<"workspaces">; | |
| 1255 | + }, | |
| 1256 | + any | |
| 1257 | + >; | |
| 1258 | + listNamespaces: FunctionReference< | |
| 1259 | + "query", | |
| 1260 | + "internal", | |
| 1261 | + { buildId: string; limit: number; workspaceId: Id<"workspaces"> }, | |
| 1262 | + any | |
| 1263 | + >; | |
| 1264 | + listToolsByNamespace: FunctionReference< | |
| 1265 | + "query", | |
| 1266 | + "internal", | |
| 1267 | + { | |
| 1268 | + buildId: string; | |
| 1269 | + limit: number; | |
| 1270 | + namespace: string; | |
| 1271 | + workspaceId: Id<"workspaces">; | |
| 1272 | + }, | |
| 1273 | + any | |
| 1274 | + >; | |
| 1275 | + putNamespacesBatch: FunctionReference< | |
| 1276 | + "mutation", | |
| 1277 | + "internal", | |
| 1278 | + { | |
| 1279 | + buildId: string; | |
| 1280 | + namespaces: Array<{ | |
| 1281 | + namespace: string; | |
| 1282 | + samplePaths: Array<string>; | |
| 1283 | + toolCount: number; | |
| 1284 | + }>; | |
| 1285 | + workspaceId: Id<"workspaces">; | |
| 1286 | + }, | |
| 1287 | + any | |
| 1288 | + >; | |
| 1289 | + putToolsBatch: FunctionReference< | |
| 1290 | + "mutation", | |
| 1291 | + "internal", | |
| 1292 | + { | |
| 1293 | + buildId: string; | |
| 1294 | + tools: Array<{ | |
| 1295 | + aliases: Array<string>; | |
| 1296 | + approval: "auto" | "required"; | |
| 1297 | + description: string; | |
| 1298 | + displayInput?: string; | |
| 1299 | + displayOutput?: string; | |
| 1300 | + namespace: string; | |
| 1301 | + normalizedPath: string; | |
| 1302 | + path: string; | |
| 1303 | + preferredPath: string; | |
| 1304 | + previewInputKeys?: Array<string>; | |
| 1305 | + requiredInputKeys?: Array<string>; | |
| 1306 | + searchText: string; | |
| 1307 | + serializedToolJson: string; | |
| 1308 | + source?: string; | |
| 1309 | + typedRef?: { | |
| 1310 | + kind: "openapi_operation"; | |
| 1311 | + operationId: string; | |
| 1312 | + sourceKey: string; | |
| 1313 | + }; | |
| 1314 | + }>; | |
| 1315 | + workspaceId: Id<"workspaces">; | |
| 1316 | + }, | |
| 1317 | + any | |
| 1318 | + >; | |
| 1319 | + searchTools: FunctionReference< | |
| 1320 | + "query", | |
| 1321 | + "internal", | |
| 1322 | + { | |
| 1323 | + buildId: string; | |
| 1324 | + limit: number; | |
| 1325 | + query: string; | |
| 1326 | + workspaceId: Id<"workspaces">; | |
| 1327 | + }, | |
| 1328 | + any | |
| 1329 | + >; | |
| 1330 | + }; | |
| 1222 | 1331 | workspaceAuthInternal: { |
| 1223 | 1332 | getWorkspaceAccessForAnonymousSubject: FunctionReference< |
| 1224 | 1333 | "query", |
| @@ -21,6 +21,7 @@ function setup() { · 20 unmodified lines | ||
| 21 | 21 | "./app.ts": () => import("./app"), |
| 22 | 22 | "./billingInternal.ts": () => import("./billingInternal"), |
| 23 | 23 | "./billingSync.ts": () => import("./billingSync"), |
| 24 | + "./toolRegistry.ts": () => import("./toolRegistry"), | |
| 24 | 25 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 25 | 26 | }); |
| 26 | 27 | } |
| @@ -7,6 +7,7 @@ import schema from "./schema"; · 6 unmodified lines | ||
| 7 | 7 | function setup() { |
| 8 | 8 | return convexTest(schema, { |
| 9 | 9 | "./database.ts": () => import("./database"), |
| 10 | + "./toolRegistry.ts": () => import("./toolRegistry"), | |
| 10 | 11 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 11 | 12 | }); |
| 12 | 13 | } |
| @@ -15,6 +15,7 @@ function setup() { · 14 unmodified lines | ||
| 15 | 15 | "./auth.ts": () => import("./auth"), |
| 16 | 16 | "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"), |
| 17 | 17 | "./workspaceToolCache.ts": () => import("./workspaceToolCache"), |
| 18 | + "./toolRegistry.ts": () => import("./toolRegistry"), | |
| 18 | 19 | "./openApiSpecCache.ts": () => import("./openApiSpecCache"), |
| 19 | 20 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 20 | 21 | }); |
| @@ -12,6 +12,7 @@ function setup() { · 11 unmodified lines | ||
| 12 | 12 | "./executorNode.ts": () => import("./executorNode"), |
| 13 | 13 | "./workspaceAuthInternal.ts": () => import("./workspaceAuthInternal"), |
| 14 | 14 | "./workspaceToolCache.ts": () => import("./workspaceToolCache"), |
| 15 | + "./toolRegistry.ts": () => import("./toolRegistry"), | |
| 15 | 16 | "./openApiSpecCache.ts": () => import("./openApiSpecCache"), |
| 16 | 17 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 17 | 18 | }); |
| @@ -16,6 +16,7 @@ function setup() { · 15 unmodified lines | ||
| 16 | 16 | "../../auth.ts": () => import("../../auth"), |
| 17 | 17 | "../../workspaceAuthInternal.ts": () => import("../../workspaceAuthInternal"), |
| 18 | 18 | "../../workspaceToolCache.ts": () => import("../../workspaceToolCache"), |
| 19 | + "../../toolRegistry.ts": () => import("../../toolRegistry"), | |
| 19 | 20 | "../../openApiSpecCache.ts": () => import("../../openApiSpecCache"), |
| 20 | 21 | "../../_generated/api.js": () => import("../../_generated/api.js"), |
| 21 | 22 | }); |
| @@ -6,6 +6,7 @@ import schema from "./schema"; · 5 unmodified lines | ||
| 6 | 6 | function setup() { |
| 7 | 7 | return convexTest(schema, { |
| 8 | 8 | "./openApiSpecCache.ts": () => import("./openApiSpecCache"), |
| 9 | + "./toolRegistry.ts": () => import("./toolRegistry"), | |
| 9 | 10 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 10 | 11 | }); |
| 11 | 12 | } |
| @@ -1,17 +1,19 @@ | ||
| 1 | 1 | "use node"; |
| 2 | 2 | |
| 3 | 3 | import type { ActionCtx } from "../_generated/server"; |
| 4 | +import { internal } from "../_generated/api"; | |
| 4 | 5 | import { parseGraphqlOperationPaths } from "../../core/src/graphql/operation-paths"; |
| 5 | 6 | import type { AccessPolicyRecord, PolicyDecision, TaskRecord, ToolDefinition } from "../../core/src/types"; |
| 7 | +import { rehydrateTools, type SerializedTool } from "../../core/src/tool/source-serialization"; | |
| 6 | 8 | import { getDecisionForContext, getToolDecision } from "./policy"; |
| 7 | −import { resolveAliasedToolPath, resolveClosestToolPath, suggestToolPaths, toPreferredToolPath } from "./tool_paths"; | |
| 8 | −import { baseTools, getWorkspaceTools } from "./workspace_tools"; | |
| 9 | +import { normalizeToolPathForLookup, toPreferredToolPath } from "./tool_paths"; | |
| 10 | +import { baseTools } from "./workspace_tools"; | |
| 9 | 11 | |
| 10 | 12 | export function getGraphqlDecision( |
| 11 | 13 | task: TaskRecord, |
| 12 | 14 | tool: ToolDefinition, |
| 13 | 15 | input: unknown, |
| 14 | − workspaceTools: Map<string, ToolDefinition>, | |
| 16 | + workspaceTools: Map<string, ToolDefinition> | undefined, | |
| 15 | 17 | policies: AccessPolicyRecord[], |
| 16 | 18 | ): { decision: PolicyDecision; effectivePaths: string[] } { |
| 17 | 19 | const sourceName = tool._graphqlSource!; |
| @@ -30,7 +32,7 @@ export function getGraphqlDecision( · 12 unmodified lines | ||
| 30 | 32 | let worstDecision: PolicyDecision = "allow"; |
| 31 | 33 | |
| 32 | 34 | for (const fieldPath of fieldPaths) { |
| 33 | − const pseudoTool = workspaceTools.get(fieldPath); | |
| 35 | + const pseudoTool = workspaceTools?.get(fieldPath); | |
| 34 | 36 | const fieldDecision = pseudoTool |
| 35 | 37 | ? getDecisionForContext( |
| 36 | 38 | pseudoTool, |
| @@ -63,31 +65,43 @@ export function getGraphqlDecision( · 26 unmodified lines | ||
| 63 | 65 | return { decision: worstDecision, effectivePaths: fieldPaths }; |
| 64 | 66 | } |
| 65 | 67 | |
| 66 | −function unknownToolErrorMessage(toolPath: string, availableTools: Map<string, ToolDefinition>): string { | |
| 67 | − const suggestions = suggestToolPaths(toolPath, availableTools); | |
| 68 | − const queryHint = toolPath | |
| 69 | − .split(".") | |
| 70 | − .filter(Boolean) | |
| 71 | − .join(" "); | |
| 72 | − const suggestionText = suggestions.length > 0 | |
| 73 | − ? `\nDid you mean: ${suggestions.map((path) => `tools.${toPreferredToolPath(path)}`).join(", ")}` | |
| 74 | − : ""; | |
| 75 | − const discoverHint = `\nTry: const found = await tools.discover({ query: "${queryHint}", compact: false, depth: 2, limit: 12 });`; | |
| 76 | − return `Unknown tool: ${toolPath}${suggestionText}${discoverHint}`; | |
| 68 | +async function resolveRegistryBuildId( | |
| 69 | + ctx: ActionCtx, | |
| 70 | + workspaceId: TaskRecord["workspaceId"], | |
| 71 | +): Promise<string> { | |
| 72 | + const state = await ctx.runQuery(internal.toolRegistry.getState, { workspaceId }) as | |
| 73 | + | null | |
| 74 | + | { readyBuildId?: string }; | |
| 75 | + const buildId = state?.readyBuildId; | |
| 76 | + if (!buildId) { | |
| 77 | + throw new Error("Tool registry is not ready yet. Open Tools in the UI to build the registry."); | |
| 78 | + } | |
| 79 | + return buildId; | |
| 77 | 80 | } |
| 78 | 81 | |
| 79 | −export async function ensureWorkspaceTools( | |
| 82 | +async function suggestFromRegistry( | |
| 80 | 83 | ctx: ActionCtx, |
| 81 | − task: TaskRecord, | |
| 82 | − workspaceTools?: Map<string, ToolDefinition>, | |
| 83 | −): Promise<Map<string, ToolDefinition>> { | |
| 84 | − if (workspaceTools) { | |
| 85 | − return workspaceTools; | |
| 86 | − } | |
| 87 | − const result = await getWorkspaceTools(ctx, task.workspaceId, { | |
| 88 | − actorId: task.actorId, | |
| 89 | − }); | |
| 90 | − return result.tools; | |
| 84 | + workspaceId: TaskRecord["workspaceId"], | |
| 85 | + buildId: string, | |
| 86 | + toolPath: string, | |
| 87 | +): Promise<string[]> { | |
| 88 | + const term = toolPath.split(".").filter(Boolean).join(" "); | |
| 89 | + const hits = await ctx.runQuery(internal.toolRegistry.searchTools, { | |
| 90 | + workspaceId, | |
| 91 | + buildId, | |
| 92 | + query: term, | |
| 93 | + limit: 3, | |
| 94 | + }) as Array<{ preferredPath: string }>; | |
| 95 | + return hits.map((hit) => hit.preferredPath); | |
| 96 | +} | |
| 97 | + | |
| 98 | +function unknownToolErrorMessage(toolPath: string, suggestions: string[]): string { | |
| 99 | + const suggestionText = suggestions.length > 0 | |
| 100 | + ? `\nDid you mean: ${suggestions.map((path) => `tools.${path}`).join(", ")}` | |
| 101 | + : ""; | |
| 102 | + const queryHint = toolPath.split(".").filter(Boolean).join(" "); | |
| 103 | + const discoverHint = `\nTry: const found = await tools.discover({ query: "${queryHint}", compact: true, depth: 1, limit: 12 });`; | |
| 104 | + return `Unknown tool: ${toolPath}${suggestionText}${discoverHint}`; | |
| 91 | 105 | } |
| 92 | 106 | |
| 93 | 107 | export async function resolveToolForCall( |
| @@ -97,41 +111,48 @@ export async function resolveToolForCall( · 3 unmodified lines | ||
| 97 | 111 | ): Promise<{ |
| 98 | 112 | tool: ToolDefinition; |
| 99 | 113 | resolvedToolPath: string; |
| 100 | − workspaceTools?: Map<string, ToolDefinition>; | |
| 101 | 114 | }> { |
| 102 | − let workspaceTools: Map<string, ToolDefinition> | undefined; | |
| 115 | + const builtin = baseTools.get(toolPath); | |
| 116 | + if (builtin) { | |
| 117 | + return { tool: builtin, resolvedToolPath: toolPath }; | |
| 118 | + } | |
| 119 | + | |
| 120 | + const buildId = await resolveRegistryBuildId(ctx, task.workspaceId); | |
| 121 | + | |
| 103 | 122 | let resolvedToolPath = toolPath; |
| 104 | − let tool = baseTools.get(toolPath); | |
| 105 | − if (!tool) { | |
| 106 | − workspaceTools = await ensureWorkspaceTools(ctx, task, workspaceTools); | |
| 107 | − tool = workspaceTools.get(toolPath); | |
| 108 | − | |
| 109 | − if (!tool) { | |
| 110 | − const aliasedPath = resolveAliasedToolPath(toolPath, workspaceTools); | |
| 111 | − if (aliasedPath) { | |
| 112 | − resolvedToolPath = aliasedPath; | |
| 113 | − tool = workspaceTools.get(aliasedPath); | |
| 114 | − } | |
| 123 | + let entry = await ctx.runQuery(internal.toolRegistry.getToolByPath, { | |
| 124 | + workspaceId: task.workspaceId, | |
| 125 | + buildId, | |
| 126 | + path: toolPath, | |
| 127 | + }) as null | { path: string; serializedToolJson: string }; | |
| 128 | + | |
| 129 | + if (!entry) { | |
| 130 | + const normalized = normalizeToolPathForLookup(toolPath); | |
| 131 | + const hits = await ctx.runQuery(internal.toolRegistry.getToolsByNormalizedPath, { | |
| 132 | + workspaceId: task.workspaceId, | |
| 133 | + buildId, | |
| 134 | + normalizedPath: normalized, | |
| 135 | + limit: 5, | |
| 136 | + }) as Array<{ path: string; serializedToolJson: string }>; | |
| 137 | + | |
| 138 | + if (hits.length > 0) { | |
| 139 | + // Prefer exact match on preferred path formatting, otherwise shortest canonical path. | |
| 140 | + const exact = hits.find((hit) => toPreferredToolPath(hit.path) === toPreferredToolPath(toolPath)); | |
| 141 | + entry = exact ?? hits.sort((a, b) => a.path.length - b.path.length || a.path.localeCompare(b.path))[0]!; | |
| 142 | + resolvedToolPath = entry.path; | |
| 115 | 143 | } |
| 116 | 144 | } |
| 117 | 145 | |
| 118 | − if (!tool) { | |
| 119 | − const availableTools = workspaceTools ?? baseTools; | |
| 120 | − const healedPath = resolveClosestToolPath(toolPath, availableTools); | |
| 121 | − if (healedPath) { | |
| 122 | − resolvedToolPath = healedPath; | |
| 123 | − tool = availableTools.get(healedPath); | |
| 124 | − } | |
| 146 | + if (!entry) { | |
| 147 | + const suggestions = await suggestFromRegistry(ctx, task.workspaceId, buildId, toolPath); | |
| 148 | + throw new Error(unknownToolErrorMessage(toolPath, suggestions)); | |
| 125 | 149 | } |
| 126 | 150 | |
| 151 | + const serialized = JSON.parse(entry.serializedToolJson) as SerializedTool; | |
| 152 | + const [tool] = rehydrateTools([serialized], baseTools); | |
| 127 | 153 | if (!tool) { |
| 128 | − const availableTools = workspaceTools ?? baseTools; | |
| 129 | − throw new Error(unknownToolErrorMessage(toolPath, availableTools)); | |
| 154 | + throw new Error(`Failed to rehydrate tool: ${resolvedToolPath}`); | |
| 130 | 155 | } |
| 131 | 156 | |
| 132 | − return { | |
| 133 | − tool, | |
| 134 | − resolvedToolPath, | |
| 135 | − workspaceTools, | |
| 136 | − }; | |
| 157 | + return { tool, resolvedToolPath }; | |
| 137 | 158 | } |
| @@ -8,18 +8,19 @@ import type { · 7 unmodified lines | ||
| 8 | 8 | PolicyDecision, |
| 9 | 9 | ResolvedToolCredential, |
| 10 | 10 | TaskRecord, |
| 11 | + ToolDefinition, | |
| 11 | 12 | ToolCallRecord, |
| 12 | 13 | ToolCallRequest, |
| 13 | 14 | ToolRunContext, |
| 14 | 15 | } from "../../core/src/types"; |
| 15 | 16 | import { describeError } from "../../core/src/utils"; |
| 16 | 17 | import { asPayload } from "../lib/object"; |
| 17 | −import { getToolDecision, isToolAllowedForTask } from "./policy"; | |
| 18 | +import { getToolDecision, getDecisionForContext } from "./policy"; | |
| 18 | 19 | import { baseTools } from "./workspace_tools"; |
| 19 | 20 | import { publishTaskEvent } from "./events"; |
| 20 | 21 | import { completeToolCall, denyToolCall, failToolCall } from "./tool_call_lifecycle"; |
| 21 | 22 | import { assertPersistedCallRunnable, resolveCredentialHeaders } from "./tool_call_credentials"; |
| 22 | −import { ensureWorkspaceTools, getGraphqlDecision, resolveToolForCall } from "./tool_call_resolution"; | |
| 23 | +import { getGraphqlDecision, resolveToolForCall } from "./tool_call_resolution"; | |
| 23 | 24 | |
| 24 | 25 | function createApprovalId(): string { |
| 25 | 26 | return `approval_${crypto.randomUUID()}`; |
| @@ -57,13 +58,151 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 31 unmodified lines | ||
| 57 | 58 | const policies = await ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: task.workspaceId }); |
| 58 | 59 | const typedPolicies = policies as AccessPolicyRecord[]; |
| 59 | 60 | |
| 60 | − let { tool, resolvedToolPath, workspaceTools } = await resolveToolForCall(ctx, task, toolPath); | |
| 61 | + // Fast system tools are handled server-side from the registry. | |
| 62 | + if (toolPath === "discover" || toolPath === "catalog.namespaces" || toolPath === "catalog.tools") { | |
| 63 | + const toolRegistry = internal.toolRegistry; | |
| 64 | + const state = await ctx.runQuery(toolRegistry.getState, { workspaceId: task.workspaceId }) as | |
| 65 | + | null | |
| 66 | + | { readyBuildId?: string }; | |
| 67 | + const buildId = state?.readyBuildId; | |
| 68 | + if (!buildId) { | |
| 69 | + throw new Error("Tool registry is not ready yet. Open Tools in the UI to build the registry."); | |
| 70 | + } | |
| 71 | + | |
| 72 | + const payload = input && typeof input === "object" ? (input as Record<string, unknown>) : {}; | |
| 73 | + const isAllowed = (path: string, approval: ToolDefinition["approval"]) => | |
| 74 | + getDecisionForContext( | |
| 75 | + { path, approval, description: "", run: async () => null } as ToolDefinition, | |
| 76 | + { workspaceId: task.workspaceId, actorId: task.actorId, clientId: task.clientId }, | |
| 77 | + typedPolicies, | |
| 78 | + ) !== "deny"; | |
| 79 | + | |
| 80 | + const formatSignature = (inputHint: string | undefined, outputHint: string | undefined) => { | |
| 81 | + const args = (inputHint && inputHint.trim().length > 0) ? inputHint.trim() : "{}"; | |
| 82 | + const out = (outputHint && outputHint.trim().length > 0) ? outputHint.trim() : "unknown"; | |
| 83 | + return `(input: ${args}): Promise<${out}>`; | |
| 84 | + }; | |
| 85 | + | |
| 86 | + const buildExampleCall = (path: string, previewKeys?: string[]) => { | |
| 87 | + const keys = Array.isArray(previewKeys) ? previewKeys : []; | |
| 88 | + if (keys.length === 0) { | |
| 89 | + return `await tools.${path}({});`; | |
| 90 | + } | |
| 91 | + const snippet = keys | |
| 92 | + .slice(0, 5) | |
| 93 | + .map((key) => `${key}: ...`) | |
| 94 | + .join(", "); | |
| 95 | + return `await tools.${path}({ ${snippet} });`; | |
| 96 | + }; | |
| 97 | + | |
| 98 | + if (toolPath === "catalog.namespaces") { | |
| 99 | + const limit = Math.max(1, Math.min(200, Number(payload.limit ?? 200))); | |
| 100 | + const namespaces = await ctx.runQuery(toolRegistry.listNamespaces, { | |
| 101 | + workspaceId: task.workspaceId, | |
| 102 | + buildId, | |
| 103 | + limit, | |
| 104 | + }) as Array<{ namespace: string; toolCount: number; samplePaths: string[] }>; | |
| 105 | + return { namespaces, total: namespaces.length }; | |
| 106 | + } | |
| 107 | + | |
| 108 | + if (toolPath === "catalog.tools") { | |
| 109 | + const namespace = String(payload.namespace ?? "").trim().toLowerCase(); | |
| 110 | + const query = String(payload.query ?? "").trim(); | |
| 111 | + const limit = Math.max(1, Math.min(200, Number(payload.limit ?? 50))); | |
| 112 | + | |
| 113 | + const raw = query | |
| 114 | + ? await ctx.runQuery(toolRegistry.searchTools, { | |
| 115 | + workspaceId: task.workspaceId, | |
| 116 | + buildId, | |
| 117 | + query, | |
| 118 | + limit, | |
| 119 | + }) | |
| 120 | + : namespace | |
| 121 | + ? await ctx.runQuery(toolRegistry.listToolsByNamespace, { | |
| 122 | + workspaceId: task.workspaceId, | |
| 123 | + buildId, | |
| 124 | + namespace, | |
| 125 | + limit, | |
| 126 | + }) | |
| 127 | + : []; | |
| 128 | + | |
| 129 | + const results = (raw as Array<any>) | |
| 130 | + .filter((entry) => !namespace || String(entry.preferredPath ?? entry.path ?? "").toLowerCase().startsWith(`${namespace}.`)) | |
| 131 | + .filter((entry) => isAllowed(entry.path, entry.approval)) | |
| 132 | + .slice(0, limit) | |
| 133 | + .map((entry) => { | |
| 134 | + const preferredPath = entry.preferredPath ?? entry.path; | |
| 135 | + return { | |
| 136 | + path: preferredPath, | |
| 137 | + aliases: entry.aliases ?? [], | |
| 138 | + source: entry.source, | |
| 139 | + approval: entry.approval, | |
| 140 | + description: entry.description, | |
| 141 | + signatureInfo: { | |
| 142 | + input: entry.displayInput, | |
| 143 | + output: entry.displayOutput, | |
| 144 | + requiredKeys: entry.requiredInputKeys ?? [], | |
| 145 | + previewKeys: entry.previewInputKeys ?? [], | |
| 146 | + }, | |
| 147 | + signatureText: formatSignature(entry.displayInput, entry.displayOutput), | |
| 148 | + canonicalSignature: formatSignature(entry.displayInput, entry.displayOutput), | |
| 149 | + exampleCall: buildExampleCall(preferredPath, entry.previewInputKeys), | |
| 150 | + }; | |
| 151 | + }); | |
| 152 | + | |
| 153 | + return { results, total: results.length }; | |
| 154 | + } | |
| 155 | + | |
| 156 | + // discover | |
| 157 | + const query = String(payload.query ?? "").trim(); | |
| 158 | + const limit = Math.max(1, Math.min(50, Number(payload.limit ?? 8))); | |
| 159 | + const compact = payload.compact === false ? false : true; | |
| 160 | + const hits = await ctx.runQuery(toolRegistry.searchTools, { | |
| 161 | + workspaceId: task.workspaceId, | |
| 162 | + buildId, | |
| 163 | + query, | |
| 164 | + limit: Math.max(limit * 2, limit), | |
| 165 | + }) as Array<any>; | |
| 166 | + | |
| 167 | + const filtered = hits | |
| 168 | + .filter((entry) => isAllowed(entry.path, entry.approval)) | |
| 169 | + .slice(0, limit); | |
| 170 | + | |
| 171 | + const results = filtered.map((entry) => { | |
| 172 | + const preferredPath = entry.preferredPath ?? entry.path; | |
| 173 | + const description = compact ? String(entry.description ?? "").split("\n")[0] : entry.description; | |
| 174 | + return { | |
| 175 | + path: preferredPath, | |
| 176 | + aliases: entry.aliases ?? [], | |
| 177 | + source: entry.source, | |
| 178 | + approval: entry.approval, | |
| 179 | + description, | |
| 180 | + signature: formatSignature(entry.displayInput, entry.displayOutput), | |
| 181 | + canonicalSignature: formatSignature(entry.displayInput, entry.displayOutput), | |
| 182 | + signatureInfo: { | |
| 183 | + input: entry.displayInput, | |
| 184 | + output: entry.displayOutput, | |
| 185 | + requiredKeys: entry.requiredInputKeys ?? [], | |
| 186 | + previewKeys: entry.previewInputKeys ?? [], | |
| 187 | + }, | |
| 188 | + exampleCall: buildExampleCall(preferredPath, entry.previewInputKeys), | |
| 189 | + }; | |
| 190 | + }); | |
| 191 | + | |
| 192 | + const bestPath = results[0]?.path ?? null; | |
| 193 | + return { | |
| 194 | + bestPath, | |
| 195 | + results, | |
| 196 | + total: results.length, | |
| 197 | + }; | |
| 198 | + } | |
| 199 | + | |
| 200 | + let { tool, resolvedToolPath } = await resolveToolForCall(ctx, task, toolPath); | |
| 61 | 201 | |
| 62 | 202 | let decision: PolicyDecision; |
| 63 | 203 | let effectiveToolPath = resolvedToolPath; |
| 64 | 204 | if (tool._graphqlSource) { |
| 65 | − workspaceTools = await ensureWorkspaceTools(ctx, task, workspaceTools); | |
| 66 | − const result = getGraphqlDecision(task, tool, input, workspaceTools, typedPolicies); | |
| 205 | + const result = getGraphqlDecision(task, tool, input, undefined, typedPolicies); | |
| 67 | 206 | decision = result.decision; |
| 68 | 207 | if (result.effectivePaths.length > 0) { |
| 69 | 208 | effectiveToolPath = result.effectivePaths.join(", "); |
| @@ -179,7 +318,8 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 109 unmodified lines | ||
| 179 | 318 | actorId: task.actorId, |
| 180 | 319 | clientId: task.clientId, |
| 181 | 320 | credential, |
| 182 | − isToolAllowed: (path) => isToolAllowedForTask(task, path, workspaceTools ?? baseTools, typedPolicies), | |
| 321 | + // Tool visibility is enforced server-side; runtime tool implementations don't use this. | |
| 322 | + isToolAllowed: (_path) => true, | |
| 183 | 323 | }; |
| 184 | 324 | const value = await tool.run(input, context); |
| 185 | 325 | await completeToolCall(ctx, { |
| @@ -46,7 +46,7 @@ function normalizeToolPathSegment(segment: string, isNamespace = false): string · 45 unmodified lines | ||
| 46 | 46 | return collapsed.join(""); |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | −function normalizeToolPath(path: string): string { | |
| 49 | +export function normalizeToolPathForLookup(path: string): string { | |
| 50 | 50 | const segments = path |
| 51 | 51 | .split(".") |
| 52 | 52 | .filter(Boolean); |
| @@ -110,12 +110,12 @@ export function resolveAliasedToolPath( · 57 unmodified lines | ||
| 110 | 110 | ): string | null { |
| 111 | 111 | if (toolMap.has(requestedPath)) return requestedPath; |
| 112 | 112 | |
| 113 | − const normalizedRequested = normalizeToolPath(requestedPath); | |
| 113 | + const normalizedRequested = normalizeToolPathForLookup(requestedPath); | |
| 114 | 114 | if (!normalizedRequested) return null; |
| 115 | 115 | |
| 116 | 116 | const matches: string[] = []; |
| 117 | 117 | for (const path of toolMap.keys()) { |
| 118 | − if (normalizeToolPath(path) === normalizedRequested) { | |
| 118 | + if (normalizeToolPathForLookup(path) === normalizedRequested) { | |
| 119 | 119 | matches.push(path); |
| 120 | 120 | } |
| 121 | 121 | } |
| @@ -134,13 +134,13 @@ export function suggestToolPaths( · 12 unmodified lines | ||
| 134 | 134 | toolMap: Map<string, ToolDefinition>, |
| 135 | 135 | limit = 3, |
| 136 | 136 | ): string[] { |
| 137 | − const normalizedRequested = normalizeToolPath(requestedPath); | |
| 137 | + const normalizedRequested = normalizeToolPathForLookup(requestedPath); | |
| 138 | 138 | const requestedSegments = normalizedRequested.split(".").filter(Boolean); |
| 139 | 139 | const requestedNamespace = requestedSegments[0] ?? ""; |
| 140 | 140 | |
| 141 | 141 | return [...toolMap.keys()] |
| 142 | 142 | .map((path) => { |
| 143 | − const normalizedCandidate = normalizeToolPath(path); | |
| 143 | + const normalizedCandidate = normalizeToolPathForLookup(path); | |
| 144 | 144 | const candidateSegments = normalizedCandidate.split(".").filter(Boolean); |
| 145 | 145 | const candidateNamespace = candidateSegments[0] ?? ""; |
| 146 | 146 | |
| @@ -173,7 +173,7 @@ export function resolveClosestToolPath( · 26 unmodified lines | ||
| 173 | 173 | requestedPath: string, |
| 174 | 174 | toolMap: Map<string, ToolDefinition>, |
| 175 | 175 | ): string | null { |
| 176 | − const normalizedRequested = normalizeToolPath(requestedPath); | |
| 176 | + const normalizedRequested = normalizeToolPathForLookup(requestedPath); | |
| 177 | 177 | if (!normalizedRequested) return null; |
| 178 | 178 | |
| 179 | 179 | const requestedNamespace = normalizedRequested.split(".").filter(Boolean)[0] ?? ""; |
| @@ -182,7 +182,7 @@ export function resolveClosestToolPath( · 2 unmodified lines | ||
| 182 | 182 | |
| 183 | 183 | const ranked = [...toolMap.keys()] |
| 184 | 184 | .map((path) => { |
| 185 | − const normalizedCandidate = normalizeToolPath(path); | |
| 185 | + const normalizedCandidate = normalizeToolPathForLookup(path); | |
| 186 | 186 | const candidateNamespace = normalizedCandidate.split(".").filter(Boolean)[0] ?? ""; |
| 187 | 187 | const distance = levenshteinDistance(normalizedRequested, normalizedCandidate); |
| 188 | 188 | let score = -distance; |
| @@ -3,14 +3,16 @@ · 2 unmodified lines | ||
| 3 | 3 | import type { ActionCtx } from "../_generated/server"; |
| 4 | 4 | import { internal } from "../_generated/api"; |
| 5 | 5 | import type { Id } from "../_generated/dataModel.d.ts"; |
| 6 | −import { createCatalogTools, createDiscoverTool } from "../../core/src/tool-discovery"; | |
| 7 | 6 | import { buildWorkspaceTypeBundle } from "../../core/src/tool-typing/typebundle"; |
| 7 | +import { jsonSchemaTypeHintFallback } from "../../core/src/openapi/schema-hints"; | |
| 8 | +import { buildPreviewKeys, extractTopLevelRequiredKeys } from "../../core/src/tool-typing/schema-utils"; | |
| 8 | 9 | import { |
| 9 | 10 | materializeCompiledToolSource, |
| 10 | 11 | materializeWorkspaceSnapshot, |
| 11 | 12 | type CompiledToolSourceArtifact, |
| 12 | 13 | type WorkspaceToolSnapshot, |
| 13 | 14 | } from "../../core/src/tool-sources"; |
| 15 | +import { serializeTools } from "../../core/src/tool/source-serialization"; | |
| 14 | 16 | import type { ExternalToolSourceConfig } from "../../core/src/tool/source-types"; |
| 15 | 17 | import type { |
| 16 | 18 | AccessPolicyRecord, |
| @@ -21,6 +23,7 @@ import type { · 4 unmodified lines | ||
| 21 | 23 | } from "../../core/src/types"; |
| 22 | 24 | import { computeOpenApiSourceQuality, listVisibleToolDescriptors } from "./tool_descriptors"; |
| 23 | 25 | import { loadSourceArtifact, normalizeExternalToolSource, sourceSignature } from "./tool_source_loading"; |
| 26 | +import { normalizeToolPathForLookup } from "./tool_paths"; | |
| 24 | 27 | |
| 25 | 28 | const baseTools = new Map<string, ToolDefinition>(); |
| 26 | 29 | |
| @@ -88,6 +91,91 @@ baseTools.set("admin.delete_data", { · 61 unmodified lines | ||
| 88 | 91 | }, |
| 89 | 92 | }); |
| 90 | 93 | |
| 94 | +// System tools (discover/catalog) are resolved server-side. | |
| 95 | +// Their execution is handled in the Convex tool invocation pipeline. | |
| 96 | +baseTools.set("discover", { | |
| 97 | + path: "discover", | |
| 98 | + source: "system", | |
| 99 | + approval: "auto", | |
| 100 | + description: | |
| 101 | + "Search available tools by keyword. Returns preferred path aliases, signature hints, and ready-to-copy call examples. Compact mode is enabled by default.", | |
| 102 | + typing: { | |
| 103 | + inputSchema: { | |
| 104 | + type: "object", | |
| 105 | + properties: { | |
| 106 | + query: { type: "string" }, | |
| 107 | + depth: { type: "number" }, | |
| 108 | + limit: { type: "number" }, | |
| 109 | + compact: { type: "boolean" }, | |
| 110 | + }, | |
| 111 | + required: ["query"], | |
| 112 | + }, | |
| 113 | + outputSchema: { | |
| 114 | + type: "object", | |
| 115 | + properties: { | |
| 116 | + bestPath: {}, | |
| 117 | + results: { type: "array" }, | |
| 118 | + total: { type: "number" }, | |
| 119 | + }, | |
| 120 | + required: ["bestPath", "results", "total"], | |
| 121 | + }, | |
| 122 | + }, | |
| 123 | + run: async () => { | |
| 124 | + throw new Error("discover is handled by the server tool invocation pipeline"); | |
| 125 | + }, | |
| 126 | +}); | |
| 127 | + | |
| 128 | +baseTools.set("catalog.namespaces", { | |
| 129 | + path: "catalog.namespaces", | |
| 130 | + source: "system", | |
| 131 | + approval: "auto", | |
| 132 | + description: "List available tool namespaces with counts and sample callable paths.", | |
| 133 | + typing: { | |
| 134 | + inputSchema: { type: "object", properties: {} }, | |
| 135 | + outputSchema: { | |
| 136 | + type: "object", | |
| 137 | + properties: { | |
| 138 | + namespaces: { type: "array" }, | |
| 139 | + total: { type: "number" }, | |
| 140 | + }, | |
| 141 | + required: ["namespaces", "total"], | |
| 142 | + }, | |
| 143 | + }, | |
| 144 | + run: async () => { | |
| 145 | + throw new Error("catalog.namespaces is handled by the server tool invocation pipeline"); | |
| 146 | + }, | |
| 147 | +}); | |
| 148 | + | |
| 149 | +baseTools.set("catalog.tools", { | |
| 150 | + path: "catalog.tools", | |
| 151 | + source: "system", | |
| 152 | + approval: "auto", | |
| 153 | + description: "List tools with typed signatures. Supports namespace and query filters in one call.", | |
| 154 | + typing: { | |
| 155 | + inputSchema: { | |
| 156 | + type: "object", | |
| 157 | + properties: { | |
| 158 | + namespace: { type: "string" }, | |
| 159 | + query: { type: "string" }, | |
| 160 | + depth: { type: "number" }, | |
| 161 | + limit: { type: "number" }, | |
| 162 | + compact: { type: "boolean" }, | |
| 163 | + }, | |
| 164 | + }, | |
| 165 | + outputSchema: { | |
| 166 | + type: "object", | |
| 167 | + properties: { | |
| 168 | + results: { type: "array" }, | |
| 169 | + total: { type: "number" }, | |
| 170 | + }, | |
| 171 | + required: ["results", "total"], | |
| 172 | + }, | |
| 173 | + }, | |
| 174 | + run: async () => { | |
| 175 | + throw new Error("catalog.tools is handled by the server tool invocation pipeline"); | |
| 176 | + }, | |
| 177 | +}); | |
| 178 | + | |
| 91 | 179 | interface WorkspaceToolsResult { |
| 92 | 180 | tools: Map<string, ToolDefinition>; |
| 93 | 181 | warnings: string[]; |
| @@ -176,27 +264,227 @@ function computeSourceAuthProfiles(tools: Map<string, ToolDefinition>): Record<s · 82 unmodified lines | ||
| 176 | 264 | return profiles; |
| 177 | 265 | } |
| 178 | 266 | |
| 179 | −function mergeToolsWithCatalog(externalTools: Iterable<ToolDefinition>): Map<string, ToolDefinition> { | |
| 267 | +function mergeTools(externalTools: Iterable<ToolDefinition>): Map<string, ToolDefinition> { | |
| 180 | 268 | const merged = new Map<string, ToolDefinition>(); |
| 181 | 269 | |
| 182 | 270 | for (const tool of baseTools.values()) { |
| 183 | − if (tool.path === "discover") continue; | |
| 184 | 271 | merged.set(tool.path, tool); |
| 185 | 272 | } |
| 186 | 273 | |
| 187 | 274 | for (const tool of externalTools) { |
| 188 | − if (tool.path === "discover") continue; | |
| 189 | 275 | merged.set(tool.path, tool); |
| 190 | 276 | } |
| 277 | + return merged; | |
| 278 | +} | |
| 191 | 279 | |
| 192 | − const catalogTools = createCatalogTools([...merged.values()]); | |
| 193 | − for (const tool of catalogTools) { | |
| 194 | − merged.set(tool.path, tool); | |
| 280 | +function tokenizePathSegment(value: string): string[] { | |
| 281 | + const normalized = value | |
| 282 | + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") | |
| 283 | + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") | |
| 284 | + .toLowerCase(); | |
| 285 | + | |
| 286 | + return normalized | |
| 287 | + .split(/[^a-z0-9]+/) | |
| 288 | + .map((token) => token.trim()) | |
| 289 | + .filter((token) => token.length > 0); | |
| 290 | +} | |
| 291 | + | |
| 292 | +const GENERIC_NAMESPACE_SUFFIXES = new Set([ | |
| 293 | + "api", | |
| 294 | + "apis", | |
| 295 | + "openapi", | |
| 296 | + "sdk", | |
| 297 | + "service", | |
| 298 | + "services", | |
| 299 | +]); | |
| 300 | + | |
| 301 | +function simplifyNamespaceSegment(segment: string): string { | |
| 302 | + const tokens = tokenizePathSegment(segment); | |
| 303 | + if (tokens.length === 0) return segment; | |
| 304 | + | |
| 305 | + const collapsed: string[] = []; | |
| 306 | + for (const token of tokens) { | |
| 307 | + if (collapsed[collapsed.length - 1] === token) continue; | |
| 308 | + collapsed.push(token); | |
| 195 | 309 | } |
| 196 | 310 | |
| 197 | − const discover = createDiscoverTool([...merged.values()]); | |
| 198 | − merged.set(discover.path, discover); | |
| 199 | − return merged; | |
| 311 | + while (collapsed.length > 1) { | |
| 312 | + const last = collapsed[collapsed.length - 1]; | |
| 313 | + if (!last || !GENERIC_NAMESPACE_SUFFIXES.has(last)) break; | |
| 314 | + collapsed.pop(); | |
| 315 | + } | |
| 316 | + | |
| 317 | + return collapsed.join("_"); | |
| 318 | +} | |
| 319 | + | |
| 320 | +function preferredToolPath(path: string): string { | |
| 321 | + const segments = path.split(".").filter(Boolean); | |
| 322 | + if (segments.length === 0) return path; | |
| 323 | + | |
| 324 | + const simplifiedNamespace = simplifyNamespaceSegment(segments[0]!); | |
| 325 | + if (!simplifiedNamespace || simplifiedNamespace === segments[0]) { | |
| 326 | + return path; | |
| 327 | + } | |
| 328 | + | |
| 329 | + return [simplifiedNamespace, ...segments.slice(1)].join("."); | |
| 330 | +} | |
| 331 | + | |
| 332 | +function toCamelSegment(segment: string): string { | |
| 333 | + return segment.replace(/_+([a-z0-9])/g, (_m, char: string) => char.toUpperCase()); | |
| 334 | +} | |
| 335 | + | |
| 336 | +function getPathAliases(path: string): string[] { | |
| 337 | + const segments = path.split(".").filter(Boolean); | |
| 338 | + if (segments.length === 0) return []; | |
| 339 | + | |
| 340 | + const canonicalPath = path; | |
| 341 | + const publicPath = preferredToolPath(path); | |
| 342 | + | |
| 343 | + const aliases = new Set<string>(); | |
| 344 | + const publicSegments = publicPath.split(".").filter(Boolean); | |
| 345 | + const camelPath = publicSegments.map(toCamelSegment).join("."); | |
| 346 | + const compactPath = publicSegments.map((segment) => segment.replace(/[_-]/g, "")).join("."); | |
| 347 | + const lowerPath = publicPath.toLowerCase(); | |
| 348 | + | |
| 349 | + if (publicPath !== canonicalPath) aliases.add(publicPath); | |
| 350 | + if (camelPath !== publicPath) aliases.add(camelPath); | |
| 351 | + if (compactPath !== publicPath) aliases.add(compactPath); | |
| 352 | + if (lowerPath !== publicPath) aliases.add(lowerPath); | |
| 353 | + | |
| 354 | + return [...aliases].slice(0, 4); | |
| 355 | +} | |
| 356 | + | |
| 357 | + | |
| 358 | +function normalizeHint(type?: string): string { | |
| 359 | + return type && type.trim().length > 0 ? type : "unknown"; | |
| 360 | +} | |
| 361 | + | |
| 362 | +function isEmptyObjectSchema(schema: Record<string, unknown>): boolean { | |
| 363 | + if (Object.keys(schema).length === 0) return true; | |
| 364 | + const props = schema.properties && typeof schema.properties === "object" ? schema.properties as Record<string, unknown> : {}; | |
| 365 | + const required = Array.isArray(schema.required) ? schema.required : []; | |
| 366 | + return Object.keys(props).length === 0 && required.length === 0; | |
| 367 | +} | |
| 368 | + | |
| 369 | +async function buildWorkspaceToolRegistry( | |
| 370 | + ctx: ActionCtx, | |
| 371 | + args: { | |
| 372 | + workspaceId: Id<"workspaces">; | |
| 373 | + signature: string; | |
| 374 | + tools: ToolDefinition[]; | |
| 375 | + }, | |
| 376 | +): Promise<{ buildId: string }> { | |
| 377 | + const buildId = `toolreg_${crypto.randomUUID()}`; | |
| 378 | + await ctx.runMutation(internal.toolRegistry.beginBuild, { | |
| 379 | + workspaceId: args.workspaceId, | |
| 380 | + signature: args.signature, | |
| 381 | + buildId, | |
| 382 | + }); | |
| 383 | + | |
| 384 | + const serialized = serializeTools(args.tools); | |
| 385 | + | |
| 386 | + const entries = serialized.map((st) => { | |
| 387 | + if (st.path === "discover" || st.path.startsWith("catalog.")) { | |
| 388 | + return null; | |
| 389 | + } | |
| 390 | + const preferredPath = preferredToolPath(st.path); | |
| 391 | + const aliases = getPathAliases(st.path); | |
| 392 | + const namespace = (preferredPath.split(".")[0] ?? "default").toLowerCase(); | |
| 393 | + const normalizedPath = normalizeToolPathForLookup(st.path); | |
| 394 | + const searchText = `${st.path} ${preferredPath} ${aliases.join(" ")} ${st.description} ${st.source ?? ""}`.toLowerCase(); | |
| 395 | + | |
| 396 | + const inputSchema = (st.typing?.inputSchema && typeof st.typing.inputSchema === "object") | |
| 397 | + ? st.typing.inputSchema | |
| 398 | + : {}; | |
| 399 | + const outputSchema = (st.typing?.outputSchema && typeof st.typing.outputSchema === "object") | |
| 400 | + ? st.typing.outputSchema | |
| 401 | + : {}; | |
| 402 | + | |
| 403 | + const requiredInputKeys = Array.isArray(st.typing?.requiredInputKeys) | |
| 404 | + ? st.typing!.requiredInputKeys!.filter((v): v is string => typeof v === "string") | |
| 405 | + : extractTopLevelRequiredKeys(inputSchema as any); | |
| 406 | + const previewInputKeys = Array.isArray(st.typing?.previewInputKeys) | |
| 407 | + ? st.typing!.previewInputKeys!.filter((v): v is string => typeof v === "string") | |
| 408 | + : buildPreviewKeys(inputSchema as any); | |
| 409 | + | |
| 410 | + const displayInput = normalizeHint( | |
| 411 | + isEmptyObjectSchema(inputSchema as any) ? "{}" : jsonSchemaTypeHintFallback(inputSchema), | |
| 412 | + ); | |
| 413 | + const displayOutput = normalizeHint( | |
| 414 | + Object.keys(outputSchema as any).length === 0 ? "unknown" : jsonSchemaTypeHintFallback(outputSchema), | |
| 415 | + ); | |
| 416 | + | |
| 417 | + const typedRef = st.typing?.typedRef && st.typing.typedRef.kind === "openapi_operation" | |
| 418 | + ? { | |
| 419 | + kind: "openapi_operation" as const, | |
| 420 | + sourceKey: st.typing.typedRef.sourceKey, | |
| 421 | + operationId: st.typing.typedRef.operationId, | |
| 422 | + } | |
| 423 | + : undefined; | |
| 424 | + | |
| 425 | + return { | |
| 426 | + path: st.path, | |
| 427 | + preferredPath, | |
| 428 | + namespace, | |
| 429 | + normalizedPath, | |
| 430 | + aliases, | |
| 431 | + description: st.description, | |
| 432 | + approval: st.approval, | |
| 433 | + source: st.source, | |
| 434 | + searchText, | |
| 435 | + displayInput, | |
| 436 | + displayOutput, | |
| 437 | + requiredInputKeys, | |
| 438 | + previewInputKeys, | |
| 439 | + typedRef, | |
| 440 | + serializedToolJson: JSON.stringify(st), | |
| 441 | + }; | |
| 442 | + }); | |
| 443 | + | |
| 444 | + const filteredEntries = entries.filter((entry): entry is NonNullable<typeof entry> => Boolean(entry)); | |
| 445 | + | |
| 446 | + const namespaceMap = new Map<string, { toolCount: number; samplePaths: string[] }>(); | |
| 447 | + for (const entry of filteredEntries) { | |
| 448 | + const current = namespaceMap.get(entry.namespace) ?? { toolCount: 0, samplePaths: [] }; | |
| 449 | + current.toolCount += 1; | |
| 450 | + if (current.samplePaths.length < 6) { | |
| 451 | + current.samplePaths.push(entry.preferredPath); | |
| 452 | + } | |
| 453 | + namespaceMap.set(entry.namespace, current); | |
| 454 | + } | |
| 455 | + | |
| 456 | + const namespaces = [...namespaceMap.entries()] | |
| 457 | + .map(([namespace, meta]) => ({ | |
| 458 | + namespace, | |
| 459 | + toolCount: meta.toolCount, | |
| 460 | + samplePaths: [...meta.samplePaths].sort((a, b) => a.localeCompare(b)).slice(0, 3), | |
| 461 | + })) | |
| 462 | + .sort((a, b) => a.namespace.localeCompare(b.namespace)); | |
| 463 | + | |
| 464 | + const TOOL_BATCH = 100; | |
| 465 | + for (let i = 0; i < filteredEntries.length; i += TOOL_BATCH) { | |
| 466 | + await ctx.runMutation(internal.toolRegistry.putToolsBatch, { | |
| 467 | + workspaceId: args.workspaceId, | |
| 468 | + buildId, | |
| 469 | + tools: filteredEntries.slice(i, i + TOOL_BATCH), | |
| 470 | + }); | |
| 471 | + } | |
| 472 | + | |
| 473 | + const NS_BATCH = 100; | |
| 474 | + for (let i = 0; i < namespaces.length; i += NS_BATCH) { | |
| 475 | + await ctx.runMutation(internal.toolRegistry.putNamespacesBatch, { | |
| 476 | + workspaceId: args.workspaceId, | |
| 477 | + buildId, | |
| 478 | + namespaces: namespaces.slice(i, i + NS_BATCH), | |
| 479 | + }); | |
| 480 | + } | |
| 481 | + | |
| 482 | + await ctx.runMutation(internal.toolRegistry.finishBuild, { | |
| 483 | + workspaceId: args.workspaceId, | |
| 484 | + buildId, | |
| 485 | + }); | |
| 486 | + | |
| 487 | + return { buildId }; | |
| 200 | 488 | } |
| 201 | 489 | |
| 202 | 490 | export async function getWorkspaceTools( |
| @@ -251,7 +539,7 @@ export async function getWorkspaceTools( · 48 unmodified lines | ||
| 251 | 539 | if (blob) { |
| 252 | 540 | const snapshot = JSON.parse(await blob.text()) as WorkspaceToolSnapshot; |
| 253 | 541 | const restored = materializeWorkspaceSnapshot(snapshot); |
| 254 | − const merged = mergeToolsWithCatalog(restored); | |
| 542 | + const merged = mergeTools(restored); | |
| 255 | 543 | traceStep("cacheHydrate", cacheHydrateStartedAt); |
| 256 | 544 | |
| 257 | 545 | const typesStorageId = cacheEntry.typesStorageId as Id<"_storage"> | undefined; |
| @@ -362,7 +650,7 @@ export async function getWorkspaceTools( · 104 unmodified lines | ||
| 362 | 650 | const timedOutSources = loadedSources |
| 363 | 651 | .filter((loaded) => loaded.timedOut) |
| 364 | 652 | .map((loaded) => loaded.sourceName); |
| 365 | − const merged = mergeToolsWithCatalog(externalTools); | |
| 653 | + const merged = mergeTools(externalTools); | |
| 366 | 654 | |
| 367 | 655 | let typesStorageId: Id<"_storage"> | undefined; |
| 368 | 656 | try { |
| @@ -387,6 +675,15 @@ export async function getWorkspaceTools( · 18 unmodified lines | ||
| 387 | 675 | const snapshotWriteStartedAt = Date.now(); |
| 388 | 676 | const allTools = [...merged.values()]; |
| 389 | 677 | |
| 678 | + // Build a per-tool registry for fast discover + invocation. | |
| 679 | + const registryStartedAt = Date.now(); | |
| 680 | + await buildWorkspaceToolRegistry(ctx, { | |
| 681 | + workspaceId, | |
| 682 | + signature, | |
| 683 | + tools: allTools, | |
| 684 | + }); | |
| 685 | + traceStep("toolRegistryWrite", registryStartedAt); | |
| 686 | + | |
| 390 | 687 | // Build and store a workspace-wide Monaco type bundle. |
| 391 | 688 | const openApiDtsBySource: Record<string, string> = {}; |
| 392 | 689 | for (const loaded of loadedSources) { |
| @@ -444,6 +444,8 @@ export default defineSchema({ · 443 unmodified lines | ||
| 444 | 444 | buildId: v.string(), |
| 445 | 445 | path: v.string(), |
| 446 | 446 | preferredPath: v.string(), |
| 447 | + namespace: v.string(), | |
| 448 | + normalizedPath: v.string(), | |
| 447 | 449 | aliases: v.array(v.string()), |
| 448 | 450 | description: v.string(), |
| 449 | 451 | approval: toolApprovalMode, |
| @@ -463,22 +465,25 @@ export default defineSchema({ · 13 unmodified lines | ||
| 463 | 465 | createdAt: v.number(), |
| 464 | 466 | }) |
| 465 | 467 | .index("by_workspace_build_path", ["workspaceId", "buildId", "path"]) |
| 468 | + .index("by_workspace_build_normalized", ["workspaceId", "buildId", "normalizedPath"]) | |
| 469 | + .index("by_workspace_build_namespace", ["workspaceId", "buildId", "namespace"]) | |
| 466 | 470 | .index("by_workspace_build", ["workspaceId", "buildId"]) |
| 467 | 471 | .searchIndex("search_text", { |
| 468 | 472 | searchField: "searchText", |
| 469 | 473 | filterFields: ["workspaceId", "buildId"], |
| 470 | 474 | }), |
| 471 | 475 | |
| 472 | − // Alias -> canonical path mapping for registry lookups. | |
| 473 | − workspaceToolAliases: defineTable({ | |
| 476 | + // Precomputed namespace summaries for fast catalog.namespaces. | |
| 477 | + workspaceToolNamespaces: defineTable({ | |
| 474 | 478 | workspaceId: v.id("workspaces"), |
| 475 | 479 | buildId: v.string(), |
| 476 | − alias: v.string(), | |
| 477 | − path: v.string(), | |
| 480 | + namespace: v.string(), | |
| 481 | + toolCount: v.number(), | |
| 482 | + samplePaths: v.array(v.string()), | |
| 478 | 483 | createdAt: v.number(), |
| 479 | 484 | }) |
| 480 | − .index("by_workspace_build_alias", ["workspaceId", "buildId", "alias"]) | |
| 481 | − .index("by_workspace_build", ["workspaceId", "buildId"]), | |
| 485 | + .index("by_workspace_build", ["workspaceId", "buildId"]) | |
| 486 | + .index("by_workspace_build_namespace", ["workspaceId", "buildId", "namespace"]), | |
| 482 | 487 | |
| 483 | 488 | // Anonymous session linkage. |
| 484 | 489 | // Used to map an unauthenticated/anonymous actor to a backing `accounts` row and a |
| @@ -0,0 +1,331 @@ | ||
| 1 | +import { v } from "convex/values"; | |
| 2 | +import { internalMutation, internalQuery } from "./_generated/server"; | |
| 3 | + | |
| 4 | +export const getState = internalQuery({ | |
| 5 | + args: { | |
| 6 | + workspaceId: v.id("workspaces"), | |
| 7 | + }, | |
| 8 | + handler: async (ctx, args) => { | |
| 9 | + const entry = await ctx.db | |
| 10 | + .query("workspaceToolRegistryState") | |
| 11 | + .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 12 | + .unique(); | |
| 13 | + | |
| 14 | + if (!entry) return null; | |
| 15 | + | |
| 16 | + return { | |
| 17 | + signature: entry.signature, | |
| 18 | + readyBuildId: entry.readyBuildId, | |
| 19 | + buildingBuildId: entry.buildingBuildId, | |
| 20 | + updatedAt: entry.updatedAt, | |
| 21 | + }; | |
| 22 | + }, | |
| 23 | +}); | |
| 24 | + | |
| 25 | +export const beginBuild = internalMutation({ | |
| 26 | + args: { | |
| 27 | + workspaceId: v.id("workspaces"), | |
| 28 | + signature: v.string(), | |
| 29 | + buildId: v.string(), | |
| 30 | + }, | |
| 31 | + handler: async (ctx, args) => { | |
| 32 | + const now = Date.now(); | |
| 33 | + const existing = await ctx.db | |
| 34 | + .query("workspaceToolRegistryState") | |
| 35 | + .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 36 | + .unique(); | |
| 37 | + | |
| 38 | + if (existing) { | |
| 39 | + await ctx.db.patch(existing._id, { | |
| 40 | + signature: args.signature, | |
| 41 | + buildingBuildId: args.buildId, | |
| 42 | + updatedAt: now, | |
| 43 | + }); | |
| 44 | + return; | |
| 45 | + } | |
| 46 | + | |
| 47 | + await ctx.db.insert("workspaceToolRegistryState", { | |
| 48 | + workspaceId: args.workspaceId, | |
| 49 | + signature: args.signature, | |
| 50 | + readyBuildId: undefined, | |
| 51 | + buildingBuildId: args.buildId, | |
| 52 | + createdAt: now, | |
| 53 | + updatedAt: now, | |
| 54 | + }); | |
| 55 | + }, | |
| 56 | +}); | |
| 57 | + | |
| 58 | +export const putToolsBatch = internalMutation({ | |
| 59 | + args: { | |
| 60 | + workspaceId: v.id("workspaces"), | |
| 61 | + buildId: v.string(), | |
| 62 | + tools: v.array( | |
| 63 | + v.object({ | |
| 64 | + path: v.string(), | |
| 65 | + preferredPath: v.string(), | |
| 66 | + namespace: v.string(), | |
| 67 | + normalizedPath: v.string(), | |
| 68 | + aliases: v.array(v.string()), | |
| 69 | + description: v.string(), | |
| 70 | + approval: v.union(v.literal("auto"), v.literal("required")), | |
| 71 | + source: v.optional(v.string()), | |
| 72 | + searchText: v.string(), | |
| 73 | + displayInput: v.optional(v.string()), | |
| 74 | + displayOutput: v.optional(v.string()), | |
| 75 | + requiredInputKeys: v.optional(v.array(v.string())), | |
| 76 | + previewInputKeys: v.optional(v.array(v.string())), | |
| 77 | + typedRef: v.optional( | |
| 78 | + v.object({ | |
| 79 | + kind: v.literal("openapi_operation"), | |
| 80 | + sourceKey: v.string(), | |
| 81 | + operationId: v.string(), | |
| 82 | + }), | |
| 83 | + ), | |
| 84 | + serializedToolJson: v.string(), | |
| 85 | + }), | |
| 86 | + ), | |
| 87 | + }, | |
| 88 | + handler: async (ctx, args) => { | |
| 89 | + const now = Date.now(); | |
| 90 | + for (const tool of args.tools) { | |
| 91 | + await ctx.db.insert("workspaceToolRegistry", { | |
| 92 | + workspaceId: args.workspaceId, | |
| 93 | + buildId: args.buildId, | |
| 94 | + path: tool.path, | |
| 95 | + preferredPath: tool.preferredPath, | |
| 96 | + namespace: tool.namespace, | |
| 97 | + normalizedPath: tool.normalizedPath, | |
| 98 | + aliases: tool.aliases, | |
| 99 | + description: tool.description, | |
| 100 | + approval: tool.approval, | |
| 101 | + source: tool.source, | |
| 102 | + searchText: tool.searchText, | |
| 103 | + displayInput: tool.displayInput, | |
| 104 | + displayOutput: tool.displayOutput, | |
| 105 | + requiredInputKeys: tool.requiredInputKeys, | |
| 106 | + previewInputKeys: tool.previewInputKeys, | |
| 107 | + typedRef: tool.typedRef, | |
| 108 | + serializedToolJson: tool.serializedToolJson, | |
| 109 | + createdAt: now, | |
| 110 | + }); | |
| 111 | + } | |
| 112 | + }, | |
| 113 | +}); | |
| 114 | + | |
| 115 | +export const putNamespacesBatch = internalMutation({ | |
| 116 | + args: { | |
| 117 | + workspaceId: v.id("workspaces"), | |
| 118 | + buildId: v.string(), | |
| 119 | + namespaces: v.array( | |
| 120 | + v.object({ | |
| 121 | + namespace: v.string(), | |
| 122 | + toolCount: v.number(), | |
| 123 | + samplePaths: v.array(v.string()), | |
| 124 | + }), | |
| 125 | + ), | |
| 126 | + }, | |
| 127 | + handler: async (ctx, args) => { | |
| 128 | + const now = Date.now(); | |
| 129 | + for (const ns of args.namespaces) { | |
| 130 | + await ctx.db.insert("workspaceToolNamespaces", { | |
| 131 | + workspaceId: args.workspaceId, | |
| 132 | + buildId: args.buildId, | |
| 133 | + namespace: ns.namespace, | |
| 134 | + toolCount: ns.toolCount, | |
| 135 | + samplePaths: ns.samplePaths, | |
| 136 | + createdAt: now, | |
| 137 | + }); | |
| 138 | + } | |
| 139 | + }, | |
| 140 | +}); | |
| 141 | + | |
| 142 | +export const finishBuild = internalMutation({ | |
| 143 | + args: { | |
| 144 | + workspaceId: v.id("workspaces"), | |
| 145 | + buildId: v.string(), | |
| 146 | + }, | |
| 147 | + handler: async (ctx, args) => { | |
| 148 | + const now = Date.now(); | |
| 149 | + const state = await ctx.db | |
| 150 | + .query("workspaceToolRegistryState") | |
| 151 | + .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 152 | + .unique(); | |
| 153 | + | |
| 154 | + if (!state) { | |
| 155 | + await ctx.db.insert("workspaceToolRegistryState", { | |
| 156 | + workspaceId: args.workspaceId, | |
| 157 | + signature: "", | |
| 158 | + readyBuildId: args.buildId, | |
| 159 | + buildingBuildId: undefined, | |
| 160 | + createdAt: now, | |
| 161 | + updatedAt: now, | |
| 162 | + }); | |
| 163 | + return; | |
| 164 | + } | |
| 165 | + | |
| 166 | + if (state.buildingBuildId !== args.buildId) { | |
| 167 | + // Another build started; ignore finishing this one. | |
| 168 | + return; | |
| 169 | + } | |
| 170 | + | |
| 171 | + await ctx.db.patch(state._id, { | |
| 172 | + readyBuildId: args.buildId, | |
| 173 | + buildingBuildId: undefined, | |
| 174 | + updatedAt: now, | |
| 175 | + }); | |
| 176 | + }, | |
| 177 | +}); | |
| 178 | + | |
| 179 | +export const getToolByPath = internalQuery({ | |
| 180 | + args: { | |
| 181 | + workspaceId: v.id("workspaces"), | |
| 182 | + buildId: v.string(), | |
| 183 | + path: v.string(), | |
| 184 | + }, | |
| 185 | + handler: async (ctx, args) => { | |
| 186 | + const entry = await ctx.db | |
| 187 | + .query("workspaceToolRegistry") | |
| 188 | + .withIndex("by_workspace_build_path", (q) => | |
| 189 | + q.eq("workspaceId", args.workspaceId).eq("buildId", args.buildId).eq("path", args.path), | |
| 190 | + ) | |
| 191 | + .first(); | |
| 192 | + | |
| 193 | + if (!entry) return null; | |
| 194 | + | |
| 195 | + return { | |
| 196 | + path: entry.path, | |
| 197 | + preferredPath: entry.preferredPath, | |
| 198 | + approval: entry.approval, | |
| 199 | + namespace: entry.namespace, | |
| 200 | + aliases: entry.aliases, | |
| 201 | + description: entry.description, | |
| 202 | + source: entry.source, | |
| 203 | + displayInput: entry.displayInput, | |
| 204 | + displayOutput: entry.displayOutput, | |
| 205 | + requiredInputKeys: entry.requiredInputKeys, | |
| 206 | + previewInputKeys: entry.previewInputKeys, | |
| 207 | + serializedToolJson: entry.serializedToolJson, | |
| 208 | + }; | |
| 209 | + }, | |
| 210 | +}); | |
| 211 | + | |
| 212 | +export const listToolsByNamespace = internalQuery({ | |
| 213 | + args: { | |
| 214 | + workspaceId: v.id("workspaces"), | |
| 215 | + buildId: v.string(), | |
| 216 | + namespace: v.string(), | |
| 217 | + limit: v.number(), | |
| 218 | + }, | |
| 219 | + handler: async (ctx, args) => { | |
| 220 | + const namespace = args.namespace.trim().toLowerCase(); | |
| 221 | + const limit = Math.max(1, Math.min(200, Math.floor(args.limit))); | |
| 222 | + if (!namespace) return []; | |
| 223 | + | |
| 224 | + const entries = await ctx.db | |
| 225 | + .query("workspaceToolRegistry") | |
| 226 | + .withIndex("by_workspace_build_namespace", (q) => | |
| 227 | + q.eq("workspaceId", args.workspaceId).eq("buildId", args.buildId).eq("namespace", namespace), | |
| 228 | + ) | |
| 229 | + .take(limit); | |
| 230 | + | |
| 231 | + return entries.map((entry) => ({ | |
| 232 | + path: entry.path, | |
| 233 | + preferredPath: entry.preferredPath, | |
| 234 | + aliases: entry.aliases, | |
| 235 | + description: entry.description, | |
| 236 | + approval: entry.approval, | |
| 237 | + source: entry.source, | |
| 238 | + displayInput: entry.displayInput, | |
| 239 | + displayOutput: entry.displayOutput, | |
| 240 | + requiredInputKeys: entry.requiredInputKeys, | |
| 241 | + previewInputKeys: entry.previewInputKeys, | |
| 242 | + })); | |
| 243 | + }, | |
| 244 | +}); | |
| 245 | + | |
| 246 | +export const getToolsByNormalizedPath = internalQuery({ | |
| 247 | + args: { | |
| 248 | + workspaceId: v.id("workspaces"), | |
| 249 | + buildId: v.string(), | |
| 250 | + normalizedPath: v.string(), | |
| 251 | + limit: v.number(), | |
| 252 | + }, | |
| 253 | + handler: async (ctx, args) => { | |
| 254 | + const normalized = args.normalizedPath.trim().toLowerCase(); | |
| 255 | + if (!normalized) return []; | |
| 256 | + const limit = Math.max(1, Math.min(10, Math.floor(args.limit))); | |
| 257 | + | |
| 258 | + const entries = await ctx.db | |
| 259 | + .query("workspaceToolRegistry") | |
| 260 | + .withIndex("by_workspace_build_normalized", (q) => | |
| 261 | + q.eq("workspaceId", args.workspaceId).eq("buildId", args.buildId).eq("normalizedPath", normalized), | |
| 262 | + ) | |
| 263 | + .take(limit); | |
| 264 | + | |
| 265 | + return entries.map((entry) => ({ | |
| 266 | + path: entry.path, | |
| 267 | + preferredPath: entry.preferredPath, | |
| 268 | + approval: entry.approval, | |
| 269 | + serializedToolJson: entry.serializedToolJson, | |
| 270 | + })); | |
| 271 | + }, | |
| 272 | +}); | |
| 273 | + | |
| 274 | +export const searchTools = internalQuery({ | |
| 275 | + args: { | |
| 276 | + workspaceId: v.id("workspaces"), | |
| 277 | + buildId: v.string(), | |
| 278 | + query: v.string(), | |
| 279 | + limit: v.number(), | |
| 280 | + }, | |
| 281 | + handler: async (ctx, args) => { | |
| 282 | + const term = args.query.trim(); | |
| 283 | + if (!term) return []; | |
| 284 | + | |
| 285 | + const limit = Math.max(1, Math.min(50, Math.floor(args.limit))); | |
| 286 | + const hits = await ctx.db | |
| 287 | + .query("workspaceToolRegistry") | |
| 288 | + .withSearchIndex("search_text", (q) => | |
| 289 | + q.search("searchText", term) | |
| 290 | + .eq("workspaceId", args.workspaceId) | |
| 291 | + .eq("buildId", args.buildId), | |
| 292 | + ) | |
| 293 | + .take(limit); | |
| 294 | + | |
| 295 | + return hits.map((entry) => ({ | |
| 296 | + path: entry.path, | |
| 297 | + preferredPath: entry.preferredPath, | |
| 298 | + aliases: entry.aliases, | |
| 299 | + description: entry.description, | |
| 300 | + approval: entry.approval, | |
| 301 | + source: entry.source, | |
| 302 | + displayInput: entry.displayInput, | |
| 303 | + displayOutput: entry.displayOutput, | |
| 304 | + requiredInputKeys: entry.requiredInputKeys, | |
| 305 | + previewInputKeys: entry.previewInputKeys, | |
| 306 | + })); | |
| 307 | + }, | |
| 308 | +}); | |
| 309 | + | |
| 310 | +export const listNamespaces = internalQuery({ | |
| 311 | + args: { | |
| 312 | + workspaceId: v.id("workspaces"), | |
| 313 | + buildId: v.string(), | |
| 314 | + limit: v.number(), | |
| 315 | + }, | |
| 316 | + handler: async (ctx, args) => { | |
| 317 | + const limit = Math.max(1, Math.min(200, Math.floor(args.limit))); | |
| 318 | + const entries = await ctx.db | |
| 319 | + .query("workspaceToolNamespaces") | |
| 320 | + .withIndex("by_workspace_build", (q) => | |
| 321 | + q.eq("workspaceId", args.workspaceId).eq("buildId", args.buildId), | |
| 322 | + ) | |
| 323 | + .take(limit); | |
| 324 | + | |
| 325 | + return entries.map((entry) => ({ | |
| 326 | + namespace: entry.namespace, | |
| 327 | + toolCount: entry.toolCount, | |
| 328 | + samplePaths: entry.samplePaths, | |
| 329 | + })); | |
| 330 | + }, | |
| 331 | +}); | |
| @@ -18,24 +18,38 @@ function redactCredential<T extends { secretJson: Record<string, unknown> }>(cre · 17 unmodified lines | ||
| 18 | 18 | } |
| 19 | 19 | |
| 20 | 20 | export const bootstrapAnonymousSession = mutation({ |
| 21 | − args: { sessionId: v.optional(v.string()) }, | |
| 22 | − handler: async (ctx) => { | |
| 21 | + args: { | |
| 22 | + sessionId: v.optional(v.string()), | |
| 23 | + actorId: v.optional(v.string()), | |
| 24 | + }, | |
| 25 | + handler: async (ctx, args) => { | |
| 23 | 26 | const identity = await ctx.auth.getUserIdentity(); |
| 24 | − if (!identity) { | |
| 25 | − throw new Error("Anonymous auth token is required"); | |
| 26 | − } | |
| 27 | + const requestedSessionId = args.sessionId?.trim(); | |
| 28 | + const requestedActorId = args.actorId?.trim(); | |
| 29 | + const isAnonymousIdentityRequest = Boolean(identity && isAnonymousIdentity(identity)); | |
| 27 | 30 | |
| 28 | − if (!isAnonymousIdentity(identity)) { | |
| 31 | + if (!isAnonymousIdentityRequest && identity) { | |
| 29 | 32 | throw new Error("Anonymous auth token is required"); |
| 30 | 33 | } |
| 31 | 34 | |
| 32 | − const actorId = identity.subject; | |
| 33 | − if (!actorId.startsWith("anon_")) { | |
| 35 | + const actorId = isAnonymousIdentityRequest | |
| 36 | + ? identity!.subject | |
| 37 | + : requestedActorId; | |
| 38 | + | |
| 39 | + if (actorId && !actorId.startsWith("anon_")) { | |
| 34 | 40 | throw new Error("Anonymous token subject must use anon_* actor ids"); |
| 35 | 41 | } |
| 36 | − const stableSessionId = `anon_session_${actorId}`; | |
| 42 | + | |
| 43 | + if (!actorId && !requestedSessionId) { | |
| 44 | + throw new Error("Anonymous auth token is required"); | |
| 45 | + } | |
| 46 | + | |
| 47 | + const sessionId = actorId | |
| 48 | + ? `anon_session_${actorId}` | |
| 49 | + : requestedSessionId; | |
| 50 | + | |
| 37 | 51 | return await ctx.runMutation(internal.database.bootstrapAnonymousSession, { |
| 38 | − sessionId: stableSessionId, | |
| 52 | + sessionId, | |
| 39 | 53 | actorId, |
| 40 | 54 | clientId: "web", |
| 41 | 55 | }); |
| @@ -17,6 +17,9 @@ const tables = [ · 16 unmodified lines | ||
| 17 | 17 | "tasks", |
| 18 | 18 | "openApiSpecCache", |
| 19 | 19 | "toolSources", |
| 20 | + "workspaceToolRegistryState", | |
| 21 | + "workspaceToolRegistry", | |
| 22 | + "workspaceToolNamespaces", | |
| 20 | 23 | "workspaceMembers", |
| 21 | 24 | "workspaceToolCache", |
| 22 | 25 | "workspaces", |