- Drop 10 unused schema indexes (keep billing indexes for future Stripe use) - Remove constant fields: workspaces.plan, invites.provider - Complete workspaceId boundary typing: Id<"workspaces"> throughout lib/types, executorNode, mcp_server, session-context, and all tests - Make tasks.actorId and accessPolicies.actorId/clientId optional instead of empty-string sentinels - Replace ctx:any with ActionCtx in all executorNode.ts helper functions - Rewrite normalizeExternalToolSource with explicit typed branches (no double-casts) - Centralize actorIdForAccount in lib/identity.ts - Fix DtsStorageEntry.storageId typing to Id<"_storage">, remove as-any casts - Delete dead code: openApiSpecCache.pruneExpired, workspaceToolCache.getDtsStorageIds, deleteInvitesMissingProvider migration
Unified · Split
| @@ -9,6 +9,7 @@ import type { ConvexReactClient } from "convex/react"; · 8 unmodified lines | ||
| 9 | 9 | import { ConvexProvider } from "convex/react"; |
| 10 | 10 | import type { ReacordInstance } from "@openassistant/reacord"; |
| 11 | 11 | import { Effect, Runtime } from "effect"; |
| 12 | +import type { Id } from "@executor/convex/_generated/dataModel"; | |
| 12 | 13 | import { TaskMessage } from "../views/task-message"; |
| 13 | 14 | |
| 14 | 15 | interface AskCommandDeps { |
| @@ -29,13 +30,13 @@ export async function handleAskCommand( · 14 unmodified lines | ||
| 29 | 30 | await interaction.deferReply(); |
| 30 | 31 | |
| 31 | 32 | let agentTaskId: string; |
| 32 | − let workspaceId: string; | |
| 33 | + let workspaceId: Id<"workspaces">; | |
| 33 | 34 | try { |
| 34 | 35 | const data = await unwrap( |
| 35 | 36 | deps.api.api.tasks.post({ prompt, requesterId }), |
| 36 | 37 | ); |
| 37 | 38 | agentTaskId = data.agentTaskId; |
| 38 | − workspaceId = data.workspaceId; | |
| 39 | + workspaceId = data.workspaceId as Id<"workspaces">; | |
| 39 | 40 | } catch (error) { |
| 40 | 41 | await interaction.editReply({ |
| 41 | 42 | content: `\u274c Failed to create task: ${error instanceof Error ? error.message : String(error)}`, |
| @@ -18,6 +18,7 @@ import { · 17 unmodified lines | ||
| 18 | 18 | } from "@openassistant/reacord"; |
| 19 | 19 | import { useMutation, useQuery } from "convex/react"; |
| 20 | 20 | import { api } from "@executor/convex/_generated/api"; |
| 21 | +import type { Id } from "@executor/convex/_generated/dataModel"; | |
| 21 | 22 | |
| 22 | 23 | // --------------------------------------------------------------------------- |
| 23 | 24 | // Component |
| @@ -26,7 +27,7 @@ import { api } from "@executor/convex/_generated/api"; · 2 unmodified lines | ||
| 26 | 27 | export interface TaskMessageProps { |
| 27 | 28 | readonly agentTaskId: string; |
| 28 | 29 | readonly prompt: string; |
| 29 | − readonly workspaceId: string; | |
| 30 | + readonly workspaceId: Id<"workspaces">; | |
| 30 | 31 | } |
| 31 | 32 | |
| 32 | 33 | export function TaskMessage({ agentTaskId, prompt, workspaceId }: TaskMessageProps) { |
| @@ -5,6 +5,7 @@ · 4 unmodified lines | ||
| 5 | 5 | import { createApp } from "./routes"; |
| 6 | 6 | import { ConvexHttpClient } from "convex/browser"; |
| 7 | 7 | import { api } from "@executor/convex/_generated/api"; |
| 8 | +import type { Id } from "@executor/convex/_generated/dataModel"; | |
| 8 | 9 | |
| 9 | 10 | const PORT = Number(Bun.env.PORT ?? 3000); |
| 10 | 11 | const CONVEX_URL = Bun.env.CONVEX_URL; |
| @@ -23,11 +24,11 @@ const ANON_SESSION_ID = Bun.env.EXECUTOR_ANON_SESSION_ID?.trim(); · 12 unmodified lines | ||
| 23 | 24 | |
| 24 | 25 | // Bootstrap anonymous context on executor Convex backend |
| 25 | 26 | const convex = new ConvexHttpClient(CONVEX_URL); |
| 26 | −let ctx: { workspaceId: string; actorId: string; clientId?: string }; | |
| 27 | +let ctx: { workspaceId: Id<"workspaces">; actorId: string; clientId?: string }; | |
| 27 | 28 | |
| 28 | 29 | if (PINNED_WORKSPACE_ID && PINNED_ACTOR_ID) { |
| 29 | 30 | ctx = { |
| 30 | − workspaceId: PINNED_WORKSPACE_ID, | |
| 31 | + workspaceId: PINNED_WORKSPACE_ID as Id<"workspaces">, | |
| 31 | 32 | actorId: PINNED_ACTOR_ID, |
| 32 | 33 | clientId: PINNED_CLIENT_ID, |
| 33 | 34 | }; |
| @@ -10,6 +10,7 @@ import { createAgent } from "@assistant/core"; · 9 unmodified lines | ||
| 10 | 10 | import type { TaskEvent } from "@assistant/core"; |
| 11 | 11 | import { ConvexHttpClient } from "convex/browser"; |
| 12 | 12 | import { api } from "@executor/convex/_generated/api"; |
| 13 | +import type { Id } from "@executor/convex/_generated/dataModel"; | |
| 13 | 14 | |
| 14 | 15 | // --------------------------------------------------------------------------- |
| 15 | 16 | // Config |
| @@ -17,7 +18,7 @@ import { api } from "@executor/convex/_generated/api"; · 1 unmodified line | ||
| 17 | 18 | |
| 18 | 19 | export interface ServerOptions { |
| 19 | 20 | readonly executorUrl: string; |
| 20 | − readonly workspaceId: string; | |
| 21 | + readonly workspaceId: Id<"workspaces">; | |
| 21 | 22 | readonly actorId: string; |
| 22 | 23 | readonly clientId?: string; |
| 23 | 24 | readonly context?: string; |
| @@ -93,7 +93,7 @@ async function resolveOrganizationHint(request: NextRequest): Promise<string | u · 92 unmodified lines | ||
| 93 | 93 | try { |
| 94 | 94 | const workosOrganizationId = await convexClient.query( |
| 95 | 95 | api.organizations.resolveWorkosOrganizationId, |
| 96 | − { organizationId: organizationHint as unknown as Id<"organizations"> }, | |
| 96 | + { organizationId: organizationHint as Id<"organizations"> }, | |
| 97 | 97 | ); |
| 98 | 98 | return typeof workosOrganizationId === "string" ? workosOrganizationId : undefined; |
| 99 | 99 | } catch { |
| @@ -93,7 +93,7 @@ async function resolveOrganizationHint(request: NextRequest): Promise<string | u · 92 unmodified lines | ||
| 93 | 93 | try { |
| 94 | 94 | const workosOrganizationId = await convexClient.query( |
| 95 | 95 | api.organizations.resolveWorkosOrganizationId, |
| 96 | − { organizationId: organizationHint as unknown as Id<"organizations"> }, | |
| 96 | + { organizationId: organizationHint as Id<"organizations"> }, | |
| 97 | 97 | ); |
| 98 | 98 | return typeof workosOrganizationId === "string" ? workosOrganizationId : undefined; |
| 99 | 99 | } catch { |
| @@ -231,7 +231,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 230 unmodified lines | ||
| 231 | 231 | localStorage.setItem(SESSION_KEY, context.sessionId); |
| 232 | 232 | localStorage.setItem(ACTIVE_WORKSPACE_KEY, context.workspaceId); |
| 233 | 233 | setStoredSessionId(context.sessionId); |
| 234 | − setActiveWorkspaceId(context.workspaceId as Id<"workspaces">); | |
| 234 | + setActiveWorkspaceId(context.workspaceId); | |
| 235 | 235 | setManualGuestContext(context); |
| 236 | 236 | } catch (cause) { |
| 237 | 237 | const message = cause instanceof Error ? cause.message : "Failed to create anonymous organization"; |
| @@ -401,7 +401,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 163 unmodified lines | ||
| 401 | 401 | if (guestContext) { |
| 402 | 402 | return [ |
| 403 | 403 | { |
| 404 | − id: guestContext.workspaceId as Id<"workspaces">, | |
| 404 | + id: guestContext.workspaceId, | |
| 405 | 405 | docId: null, |
| 406 | 406 | name: "Anonymous Workspace", |
| 407 | 407 | organizationId: null, |
| @@ -829,7 +829,7 @@ export declare const internal: { · 828 unmodified lines | ||
| 829 | 829 | }, |
| 830 | 830 | any |
| 831 | 831 | >; |
| 832 | − deleteAnonymousSessionsMissingAccountId: FunctionReference< | |
| 832 | + cleanupAccessPolicyEmptyStringSentinels: FunctionReference< | |
| 833 | 833 | "mutation", |
| 834 | 834 | "internal", |
| 835 | 835 | { |
| @@ -841,7 +841,19 @@ export declare const internal: { · 5 unmodified lines | ||
| 841 | 841 | }, |
| 842 | 842 | any |
| 843 | 843 | >; |
| 844 | − deleteAnonymousSessionsMissingUserId: FunctionReference< | |
| 844 | + cleanupTaskEmptyStringSentinels: FunctionReference< | |
| 845 | + "mutation", | |
| 846 | + "internal", | |
| 847 | + { | |
| 848 | + batchSize?: number; | |
| 849 | + cursor?: string | null; | |
| 850 | + dryRun?: boolean; | |
| 851 | + fn?: string; | |
| 852 | + next?: Array<string>; | |
| 853 | + }, | |
| 854 | + any | |
| 855 | + >; | |
| 856 | + deleteAnonymousSessionsMissingAccountId: FunctionReference< | |
| 845 | 857 | "mutation", |
| 846 | 858 | "internal", |
| 847 | 859 | { |
| @@ -853,7 +865,7 @@ export declare const internal: { · 5 unmodified lines | ||
| 853 | 865 | }, |
| 854 | 866 | any |
| 855 | 867 | >; |
| 856 | − deleteInvitesMissingProvider: FunctionReference< | |
| 868 | + deleteAnonymousSessionsMissingUserId: FunctionReference< | |
| 857 | 869 | "mutation", |
| 858 | 870 | "internal", |
| 859 | 871 | { |
| @@ -921,12 +933,6 @@ export declare const internal: { · 61 unmodified lines | ||
| 921 | 933 | { maxAgeMs: number; specUrl: string; version: string }, |
| 922 | 934 | any |
| 923 | 935 | >; |
| 924 | − pruneExpired: FunctionReference< | |
| 925 | − "mutation", | |
| 926 | − "internal", | |
| 927 | − { batchSize?: number; maxAgeMs: number }, | |
| 928 | − any | |
| 929 | − >; | |
| 930 | 936 | putEntry: FunctionReference< |
| 931 | 937 | "mutation", |
| 932 | 938 | "internal", |
| @@ -954,12 +960,6 @@ export declare const internal: { · 21 unmodified lines | ||
| 954 | 960 | >; |
| 955 | 961 | }; |
| 956 | 962 | workspaceToolCache: { |
| 957 | − getDtsStorageIds: FunctionReference< | |
| 958 | − "query", | |
| 959 | − "internal", | |
| 960 | − { workspaceId: Id<"workspaces"> }, | |
| 961 | − any | |
| 962 | − >; | |
| 963 | 963 | getEntry: FunctionReference< |
| 964 | 964 | "query", |
| 965 | 965 | "internal", |
| @@ -82,7 +82,6 @@ async function seedUser( · 81 unmodified lines | ||
| 82 | 82 | organizationId, |
| 83 | 83 | slug: `ws-${subject}`, |
| 84 | 84 | name: opts.workspaceName ?? `${name}'s Workspace`, |
| 85 | − plan: "free", | |
| 86 | 85 | createdByAccountId: accountId, |
| 87 | 86 | createdAt: now, |
| 88 | 87 | updatedAt: now, |
| @@ -309,7 +309,6 @@ async function ensurePersonalWorkspace( · 308 unmodified lines | ||
| 309 | 309 | organizationId, |
| 310 | 310 | slug: `${baseSlug}-${opts.workosUserId.slice(-6)}`, |
| 311 | 311 | name: personalNames.workspaceName, |
| 312 | − plan: "free", | |
| 313 | 312 | createdByAccountId: accountId, |
| 314 | 313 | createdAt: opts.now, |
| 315 | 314 | updatedAt: opts.now, |
| @@ -480,7 +479,6 @@ const workosEventHandlers = { · 164 unmodified lines | ||
| 480 | 479 | organizationId: organization._id, |
| 481 | 480 | slug: "default", |
| 482 | 481 | name: "Default Workspace", |
| 483 | − plan: "free", | |
| 484 | 482 | createdByAccountId: organization.createdByAccountId, |
| 485 | 483 | createdAt: now, |
| 486 | 484 | updatedAt: now, |
| @@ -25,7 +25,6 @@ async function seedWorkspace(t: ReturnType<typeof setup>, name = "test-ws"): Pro · 24 unmodified lines | ||
| 25 | 25 | name, |
| 26 | 26 | slug: name, |
| 27 | 27 | organizationId: orgId, |
| 28 | − plan: "free", | |
| 29 | 28 | createdAt: Date.now(), |
| 30 | 29 | updatedAt: Date.now(), |
| 31 | 30 | }); |
| @@ -27,21 +27,6 @@ const credentialProviderValidator = v.union( · 26 unmodified lines | ||
| 27 | 27 | const toolSourceTypeValidator = v.union(v.literal("mcp"), v.literal("openapi"), v.literal("graphql")); |
| 28 | 28 | const agentTaskStatusValidator = v.union(v.literal("running"), v.literal("completed"), v.literal("failed")); |
| 29 | 29 | |
| 30 | −function normalizeOptional(value?: string): string { | |
| 31 | − if (typeof value !== "string") { | |
| 32 | − return ""; | |
| 33 | − } | |
| 34 | − const trimmed = value.trim(); | |
| 35 | − return trimmed.length > 0 ? trimmed : ""; | |
| 36 | −} | |
| 37 | − | |
| 38 | −function optionalFromNormalized(value?: string): string | undefined { | |
| 39 | − if (!value) { | |
| 40 | − return undefined; | |
| 41 | − } | |
| 42 | − return value; | |
| 43 | −} | |
| 44 | − | |
| 45 | 30 | function slugify(input: string): string { |
| 46 | 31 | const slug = input |
| 47 | 32 | .toLowerCase() |
| @@ -118,8 +103,8 @@ function mapTask(doc: Doc<"tasks">) { · 70 unmodified lines | ||
| 118 | 103 | timeoutMs: typeof doc.timeoutMs === "number" ? doc.timeoutMs : DEFAULT_TIMEOUT_MS, |
| 119 | 104 | metadata: asRecord(doc.metadata), |
| 120 | 105 | workspaceId: doc.workspaceId, |
| 121 | − actorId: optionalFromNormalized(doc.actorId), | |
| 122 | − clientId: optionalFromNormalized(doc.clientId), | |
| 106 | + actorId: doc.actorId, | |
| 107 | + clientId: doc.clientId, | |
| 123 | 108 | createdAt: doc.createdAt, |
| 124 | 109 | updatedAt: doc.updatedAt, |
| 125 | 110 | startedAt: doc.startedAt, |
| @@ -149,8 +134,8 @@ function mapPolicy(doc: Doc<"accessPolicies">) { · 23 unmodified lines | ||
| 149 | 134 | return { |
| 150 | 135 | id: doc.policyId, |
| 151 | 136 | workspaceId: doc.workspaceId, |
| 152 | − actorId: optionalFromNormalized(doc.actorId), | |
| 153 | − clientId: optionalFromNormalized(doc.clientId), | |
| 137 | + actorId: doc.actorId, | |
| 138 | + clientId: doc.clientId, | |
| 154 | 139 | toolPathPattern: doc.toolPathPattern, |
| 155 | 140 | decision: doc.decision, |
| 156 | 141 | priority: doc.priority, |
| @@ -165,7 +150,7 @@ function mapCredential(doc: Doc<"sourceCredentials">) { · 8 unmodified lines | ||
| 165 | 150 | workspaceId: doc.workspaceId, |
| 166 | 151 | sourceKey: doc.sourceKey, |
| 167 | 152 | scope: doc.scope, |
| 168 | − actorId: optionalFromNormalized(doc.actorId), | |
| 153 | + actorId: doc.actorId || undefined, | |
| 169 | 154 | provider: doc.provider ?? "managed", |
| 170 | 155 | secretJson: asRecord(doc.secretJson), |
| 171 | 156 | createdAt: doc.createdAt, |
| @@ -242,7 +227,6 @@ async function ensureAnonymousIdentity( · 70 unmodified lines | ||
| 242 | 227 | organizationId, |
| 243 | 228 | slug: `anonymous-${crypto.randomUUID().slice(0, 8)}`, |
| 244 | 229 | name: anonymousWorkspaceName, |
| 245 | − plan: "free", | |
| 246 | 230 | createdByAccountId: account._id, |
| 247 | 231 | createdAt: now, |
| 248 | 232 | updatedAt: now, |
| @@ -351,8 +335,8 @@ export const createTask = internalMutation({ · 102 unmodified lines | ||
| 351 | 335 | code: args.code, |
| 352 | 336 | runtimeId: args.runtimeId, |
| 353 | 337 | workspaceId: args.workspaceId, |
| 354 | − actorId: normalizeOptional(args.actorId), | |
| 355 | − clientId: normalizeOptional(args.clientId), | |
| 338 | + actorId: args.actorId?.trim() || undefined, | |
| 339 | + clientId: args.clientId?.trim() || undefined, | |
| 356 | 340 | status: "queued", |
| 357 | 341 | timeoutMs: args.timeoutMs ?? DEFAULT_TIMEOUT_MS, |
| 358 | 342 | metadata: asRecord(args.metadata), |
| @@ -720,7 +704,7 @@ export const bootstrapAnonymousSession = internalMutation({ · 361 unmodified lines | ||
| 720 | 704 | args: { sessionId: v.optional(v.string()) }, |
| 721 | 705 | handler: async (ctx, args) => { |
| 722 | 706 | const now = Date.now(); |
| 723 | − const requestedSessionId = normalizeOptional(args.sessionId); | |
| 707 | + const requestedSessionId = args.sessionId?.trim() || ""; | |
| 724 | 708 | const allowRequestedSessionId = requestedSessionId?.startsWith("mcp_") ?? false; |
| 725 | 709 | |
| 726 | 710 | if (requestedSessionId) { |
| @@ -814,8 +798,8 @@ export const upsertAccessPolicy = internalMutation({ · 87 unmodified lines | ||
| 814 | 798 | if (existing) { |
| 815 | 799 | await ctx.db.patch(existing._id, { |
| 816 | 800 | workspaceId: args.workspaceId, |
| 817 | − actorId: normalizeOptional(args.actorId), | |
| 818 | − clientId: normalizeOptional(args.clientId), | |
| 801 | + actorId: args.actorId?.trim() || undefined, | |
| 802 | + clientId: args.clientId?.trim() || undefined, | |
| 819 | 803 | toolPathPattern: args.toolPathPattern, |
| 820 | 804 | decision: args.decision, |
| 821 | 805 | priority: args.priority ?? 100, |
| @@ -825,8 +809,8 @@ export const upsertAccessPolicy = internalMutation({ · 3 unmodified lines | ||
| 825 | 809 | await ctx.db.insert("accessPolicies", { |
| 826 | 810 | policyId, |
| 827 | 811 | workspaceId: args.workspaceId, |
| 828 | − actorId: normalizeOptional(args.actorId), | |
| 829 | − clientId: normalizeOptional(args.clientId), | |
| 812 | + actorId: args.actorId?.trim() || undefined, | |
| 813 | + clientId: args.clientId?.trim() || undefined, | |
| 830 | 814 | toolPathPattern: args.toolPathPattern, |
| 831 | 815 | decision: args.decision, |
| 832 | 816 | priority: args.priority ?? 100, |
| @@ -877,7 +861,7 @@ export const upsertCredential = internalMutation({ · 44 unmodified lines | ||
| 877 | 861 | }, |
| 878 | 862 | handler: async (ctx, args) => { |
| 879 | 863 | const now = Date.now(); |
| 880 | − const actorId = args.scope === "actor" ? normalizeOptional(args.actorId) : ""; | |
| 864 | + const actorId = args.scope === "actor" ? (args.actorId?.trim() || "") : ""; | |
| 881 | 865 | |
| 882 | 866 | const existing = await ctx.db |
| 883 | 867 | .query("sourceCredentials") |
| @@ -970,7 +954,7 @@ export const resolveCredential = internalQuery({ · 86 unmodified lines | ||
| 970 | 954 | }, |
| 971 | 955 | handler: async (ctx, args) => { |
| 972 | 956 | if (args.scope === "actor") { |
| 973 | − const actorId = normalizeOptional(args.actorId); | |
| 957 | + const actorId = args.actorId?.trim() || ""; | |
| 974 | 958 | if (!actorId) { |
| 975 | 959 | return null; |
| 976 | 960 | } |
| @@ -4,6 +4,7 @@ import type { Id } from "./_generated/dataModel"; · 3 unmodified lines | ||
| 4 | 4 | import type { MutationCtx } from "./_generated/server"; |
| 5 | 5 | import { internalMutation } from "./_generated/server"; |
| 6 | 6 | import { workspaceMutation } from "../lib/functionBuilders"; |
| 7 | +import { actorIdForAccount } from "../lib/identity"; | |
| 7 | 8 | import type { ApprovalRecord, TaskRecord } from "../lib/types"; |
| 8 | 9 | |
| 9 | 10 | const DEFAULT_TIMEOUT_MS = 300_000; |
| @@ -20,10 +21,6 @@ async function publishTaskEvent( · 10 unmodified lines | ||
| 20 | 21 | await ctx.runMutation(internal.database.createTaskEvent, input); |
| 21 | 22 | } |
| 22 | 23 | |
| 23 | −function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }) { | |
| 24 | − return account.provider === "anonymous" ? account.providerAccountId : account._id; | |
| 25 | −} | |
| 26 | − | |
| 27 | 24 | async function createTaskRecord( |
| 28 | 25 | ctx: MutationCtx, |
| 29 | 26 | args: { |
| @@ -2,10 +2,13 @@ · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | import { v } from "convex/values"; |
| 4 | 4 | import { internal } from "./_generated/api"; |
| 5 | +import type { Id } from "./_generated/dataModel"; | |
| 5 | 6 | import { action, internalAction } from "./_generated/server"; |
| 7 | +import type { ActionCtx } from "./_generated/server"; | |
| 6 | 8 | import { InProcessExecutionAdapter } from "../lib/adapters/in_process_execution_adapter"; |
| 7 | 9 | import { resolveCredentialPayload } from "../lib/credential_providers"; |
| 8 | 10 | import { APPROVAL_DENIED_PREFIX } from "../lib/execution_constants"; |
| 11 | +import { actorIdForAccount } from "../lib/identity"; | |
| 9 | 12 | import { runCodeWithAdapter } from "../lib/runtimes/runtime_core"; |
| 10 | 13 | import { createDiscoverTool } from "../lib/tool_discovery"; |
| 11 | 14 | import { |
| @@ -17,6 +20,9 @@ import { · 5 unmodified lines | ||
| 17 | 20 | serializeTools, |
| 18 | 21 | type PreparedOpenApiSpec, |
| 19 | 22 | type ExternalToolSourceConfig, |
| 23 | + type McpToolSourceConfig, | |
| 24 | + type OpenApiToolSourceConfig, | |
| 25 | + type GraphqlToolSourceConfig, | |
| 20 | 26 | type WorkspaceToolSnapshot, |
| 21 | 27 | } from "../lib/tool_sources"; |
| 22 | 28 | import { DEFAULT_TOOLS } from "../lib/tools"; |
| @@ -173,27 +179,23 @@ function normalizeExternalToolSource(raw: { · 150 unmodified lines | ||
| 173 | 179 | name: string; |
| 174 | 180 | config: Record<string, unknown>; |
| 175 | 181 | }): ExternalToolSourceConfig { |
| 176 | − const merged = { | |
| 177 | − type: raw.type, | |
| 178 | − name: raw.name, | |
| 179 | − ...raw.config, | |
| 180 | − } as Record<string, unknown>; | |
| 182 | + const config = raw.config; | |
| 181 | 183 | |
| 182 | 184 | if (raw.type === "mcp") { |
| 183 | − if (typeof merged.url !== "string" || merged.url.trim().length === 0) { | |
| 185 | + if (typeof config.url !== "string" || config.url.trim().length === 0) { | |
| 184 | 186 | throw new Error(`MCP source '${raw.name}' missing url`); |
| 185 | 187 | } |
| 186 | 188 | |
| 187 | 189 | if ( |
| 188 | − merged.transport !== undefined | |
| 189 | − && merged.transport !== "sse" | |
| 190 | − && merged.transport !== "streamable-http" | |
| 190 | + config.transport !== undefined | |
| 191 | + && config.transport !== "sse" | |
| 192 | + && config.transport !== "streamable-http" | |
| 191 | 193 | ) { |
| 192 | 194 | throw new Error(`MCP source '${raw.name}' has invalid transport`); |
| 193 | 195 | } |
| 194 | 196 | |
| 195 | − if (merged.queryParams !== undefined) { | |
| 196 | − const queryParams = merged.queryParams; | |
| 197 | + if (config.queryParams !== undefined) { | |
| 198 | + const queryParams = config.queryParams; | |
| 197 | 199 | if (!queryParams || typeof queryParams !== "object" || Array.isArray(queryParams)) { |
| 198 | 200 | throw new Error(`MCP source '${raw.name}' queryParams must be an object`); |
| 199 | 201 | } |
| @@ -205,22 +207,52 @@ function normalizeExternalToolSource(raw: { · 5 unmodified lines | ||
| 205 | 207 | } |
| 206 | 208 | } |
| 207 | 209 | |
| 208 | − return merged as unknown as ExternalToolSourceConfig; | |
| 210 | + const result: McpToolSourceConfig = { | |
| 211 | + type: "mcp", | |
| 212 | + name: raw.name, | |
| 213 | + url: config.url, | |
| 214 | + transport: config.transport as McpToolSourceConfig["transport"], | |
| 215 | + queryParams: config.queryParams as McpToolSourceConfig["queryParams"], | |
| 216 | + defaultApproval: config.defaultApproval as McpToolSourceConfig["defaultApproval"], | |
| 217 | + overrides: config.overrides as McpToolSourceConfig["overrides"], | |
| 218 | + }; | |
| 219 | + return result; | |
| 209 | 220 | } |
| 210 | 221 | |
| 211 | 222 | if (raw.type === "graphql") { |
| 212 | − if (typeof merged.endpoint !== "string" || merged.endpoint.trim().length === 0) { | |
| 223 | + if (typeof config.endpoint !== "string" || config.endpoint.trim().length === 0) { | |
| 213 | 224 | throw new Error(`GraphQL source '${raw.name}' missing endpoint`); |
| 214 | 225 | } |
| 215 | − return merged as unknown as ExternalToolSourceConfig; | |
| 226 | + | |
| 227 | + const result: GraphqlToolSourceConfig = { | |
| 228 | + type: "graphql", | |
| 229 | + name: raw.name, | |
| 230 | + endpoint: config.endpoint, | |
| 231 | + schema: config.schema as GraphqlToolSourceConfig["schema"], | |
| 232 | + auth: config.auth as GraphqlToolSourceConfig["auth"], | |
| 233 | + defaultQueryApproval: config.defaultQueryApproval as GraphqlToolSourceConfig["defaultQueryApproval"], | |
| 234 | + defaultMutationApproval: config.defaultMutationApproval as GraphqlToolSourceConfig["defaultMutationApproval"], | |
| 235 | + overrides: config.overrides as GraphqlToolSourceConfig["overrides"], | |
| 236 | + }; | |
| 237 | + return result; | |
| 216 | 238 | } |
| 217 | 239 | |
| 218 | − const spec = merged.spec; | |
| 219 | − if (typeof spec !== "string" && typeof spec !== "object") { | |
| 240 | + const spec = config.spec; | |
| 241 | + if (typeof spec !== "string" && (typeof spec !== "object" || spec === null)) { | |
| 220 | 242 | throw new Error(`OpenAPI source '${raw.name}' missing spec`); |
| 221 | 243 | } |
| 222 | 244 | |
| 223 | − return merged as unknown as ExternalToolSourceConfig; | |
| 245 | + const result: OpenApiToolSourceConfig = { | |
| 246 | + type: "openapi", | |
| 247 | + name: raw.name, | |
| 248 | + spec: spec as OpenApiToolSourceConfig["spec"], | |
| 249 | + baseUrl: config.baseUrl as OpenApiToolSourceConfig["baseUrl"], | |
| 250 | + auth: config.auth as OpenApiToolSourceConfig["auth"], | |
| 251 | + defaultReadApproval: config.defaultReadApproval as OpenApiToolSourceConfig["defaultReadApproval"], | |
| 252 | + defaultWriteApproval: config.defaultWriteApproval as OpenApiToolSourceConfig["defaultWriteApproval"], | |
| 253 | + overrides: config.overrides as OpenApiToolSourceConfig["overrides"], | |
| 254 | + }; | |
| 255 | + return result; | |
| 224 | 256 | } |
| 225 | 257 | |
| 226 | 258 | async function sleep(ms: number): Promise<void> { |
| @@ -230,7 +262,7 @@ async function sleep(ms: number): Promise<void> { · 3 unmodified lines | ||
| 230 | 262 | const baseTools = new Map<string, ToolDefinition>(DEFAULT_TOOLS.map((tool) => [tool.path, tool])); |
| 231 | 263 | interface DtsStorageEntry { |
| 232 | 264 | sourceKey: string; |
| 233 | − storageId: string; | |
| 265 | + storageId: Id<"_storage">; | |
| 234 | 266 | } |
| 235 | 267 | |
| 236 | 268 | const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000; |
| @@ -239,7 +271,7 @@ const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000; · 2 unmodified lines | ||
| 239 | 271 | const OPENAPI_CACHE_VERSION = "v14"; |
| 240 | 272 | |
| 241 | 273 | async function publish( |
| 242 | − ctx: any, | |
| 274 | + ctx: ActionCtx, | |
| 243 | 275 | taskId: string, |
| 244 | 276 | eventName: "task" | "approval", |
| 245 | 277 | type: string, |
| @@ -253,7 +285,7 @@ async function publish( · 7 unmodified lines | ||
| 253 | 285 | }); |
| 254 | 286 | } |
| 255 | 287 | |
| 256 | −async function waitForApproval(ctx: any, approvalId: string): Promise<"approved" | "denied"> { | |
| 288 | +async function waitForApproval(ctx: ActionCtx, approvalId: string): Promise<"approved" | "denied"> { | |
| 257 | 289 | while (true) { |
| 258 | 290 | const approval = await ctx.runQuery(internal.database.getApproval, { approvalId }); |
| 259 | 291 | if (!approval) { |
| @@ -278,7 +310,7 @@ async function waitForApproval(ctx: any, approvalId: string): Promise<"approved" · 18 unmodified lines | ||
| 278 | 310 | * No size limits — Convex file storage handles multi-MB specs without issue. |
| 279 | 311 | */ |
| 280 | 312 | async function loadCachedOpenApiSpec( |
| 281 | − ctx: any, | |
| 313 | + ctx: ActionCtx, | |
| 282 | 314 | specUrl: string, |
| 283 | 315 | sourceName: string, |
| 284 | 316 | ): Promise<PreparedOpenApiSpec> { |
| @@ -327,7 +359,7 @@ async function loadCachedOpenApiSpec( · 42 unmodified lines | ||
| 327 | 359 | } |
| 328 | 360 | |
| 329 | 361 | async function loadSourceTools( |
| 330 | − ctx: any, | |
| 362 | + ctx: ActionCtx, | |
| 331 | 363 | source: ExternalToolSourceConfig, |
| 332 | 364 | ): Promise<{ tools: ToolDefinition[]; warnings: string[] }> { |
| 333 | 365 | if (source.type === "openapi" && typeof source.spec === "string") { |
| @@ -356,7 +388,7 @@ interface WorkspaceToolsResult { · 22 unmodified lines | ||
| 356 | 388 | dtsStorageIds: DtsStorageEntry[]; |
| 357 | 389 | } |
| 358 | 390 | |
| 359 | −async function getWorkspaceTools(ctx: any, workspaceId: string): Promise<WorkspaceToolsResult> { | |
| 391 | +async function getWorkspaceTools(ctx: ActionCtx, workspaceId: Id<"workspaces">): Promise<WorkspaceToolsResult> { | |
| 360 | 392 | const sources = (await ctx.runQuery(internal.database.listToolSources, { workspaceId })) |
| 361 | 393 | .filter((source: { enabled: boolean }) => source.enabled); |
| 362 | 394 | const signature = sourceSignature(workspaceId, sources); |
| @@ -440,7 +472,7 @@ async function getWorkspaceTools(ctx: any, workspaceId: string): Promise<Workspa · 77 unmodified lines | ||
| 440 | 472 | dtsEntries.map(async (entry) => { |
| 441 | 473 | const dtsBlob = new Blob([entry.content], { type: "text/plain" }); |
| 442 | 474 | const sid = await ctx.storage.store(dtsBlob); |
| 443 | − return { sourceKey: entry.sourceKey, storageId: String(sid) }; | |
| 475 | + return { sourceKey: entry.sourceKey, storageId: sid }; | |
| 444 | 476 | }), |
| 445 | 477 | ); |
| 446 | 478 | dtsStorageIds = storedDts; |
| @@ -463,7 +495,7 @@ async function getWorkspaceTools(ctx: any, workspaceId: string): Promise<Workspa · 16 unmodified lines | ||
| 463 | 495 | workspaceId, |
| 464 | 496 | signature, |
| 465 | 497 | storageId, |
| 466 | − dtsStorageIds: storedDts.map((e) => ({ sourceKey: e.sourceKey, storageId: e.storageId as any })), | |
| 498 | + dtsStorageIds: storedDts.map((e) => ({ sourceKey: e.sourceKey, storageId: e.storageId })), | |
| 467 | 499 | toolCount: allTools.length, |
| 468 | 500 | sizeBytes: json.length, |
| 469 | 501 | }); |
| @@ -608,8 +640,8 @@ function listVisibleToolDescriptors( · 138 unmodified lines | ||
| 608 | 640 | } |
| 609 | 641 | |
| 610 | 642 | async function listToolsForContext( |
| 611 | − ctx: any, | |
| 612 | − context: { workspaceId: string; actorId?: string; clientId?: string }, | |
| 643 | + ctx: ActionCtx, | |
| 644 | + context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }, | |
| 613 | 645 | ): Promise<ToolDescriptor[]> { |
| 614 | 646 | const [result, policies] = await Promise.all([ |
| 615 | 647 | getWorkspaceTools(ctx, context.workspaceId), |
| @@ -621,8 +653,8 @@ async function listToolsForContext( · 5 unmodified lines | ||
| 621 | 653 | } |
| 622 | 654 | |
| 623 | 655 | async function listToolsWithWarningsForContext( |
| 624 | − ctx: any, | |
| 625 | − context: { workspaceId: string; actorId?: string; clientId?: string }, | |
| 656 | + ctx: ActionCtx, | |
| 657 | + context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }, | |
| 626 | 658 | ): Promise<{ |
| 627 | 659 | tools: ToolDescriptor[]; |
| 628 | 660 | warnings: string[]; |
| @@ -656,10 +688,6 @@ async function listToolsWithWarningsForContext( · 27 unmodified lines | ||
| 656 | 688 | }; |
| 657 | 689 | } |
| 658 | 690 | |
| 659 | −function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }): string { | |
| 660 | − return account.provider === "anonymous" ? account.providerAccountId : account._id; | |
| 661 | −} | |
| 662 | − | |
| 663 | 691 | function isToolAllowedForTask( |
| 664 | 692 | task: TaskRecord, |
| 665 | 693 | toolPath: string, |
| @@ -672,7 +700,7 @@ function isToolAllowedForTask( · 6 unmodified lines | ||
| 672 | 700 | } |
| 673 | 701 | |
| 674 | 702 | async function resolveCredentialHeaders( |
| 675 | − ctx: any, | |
| 703 | + ctx: ActionCtx, | |
| 676 | 704 | spec: ToolCredentialSpec, |
| 677 | 705 | task: TaskRecord, |
| 678 | 706 | ): Promise<ResolvedToolCredential | null> { |
| @@ -774,7 +802,7 @@ function getGraphqlDecision( · 95 unmodified lines | ||
| 774 | 802 | return { decision: worstDecision, effectivePaths: fieldPaths }; |
| 775 | 803 | } |
| 776 | 804 | |
| 777 | −async function invokeTool(ctx: any, task: TaskRecord, call: ToolCallRequest): Promise<unknown> { | |
| 805 | +async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallRequest): Promise<unknown> { | |
| 778 | 806 | const { toolPath, input, callId } = call; |
| 779 | 807 | const policies = await ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: task.workspaceId }); |
| 780 | 808 | const typedPolicies = policies as AccessPolicyRecord[]; |
| @@ -110,13 +110,23 @@ async function verifyMcpToken( · 109 unmodified lines | ||
| 110 | 110 | } |
| 111 | 111 | } |
| 112 | 112 | |
| 113 | +/** | |
| 114 | + * Validated boundary for external workspace ID strings. | |
| 115 | + * Convex IDs are opaque strings at runtime; this cast is the single | |
| 116 | + * validated entry point so downstream code never needs ad-hoc casts. | |
| 117 | + */ | |
| 118 | +function parseWorkspaceId(raw: string): Id<"workspaces"> { | |
| 119 | + return raw as Id<"workspaces">; | |
| 120 | +} | |
| 121 | + | |
| 113 | 122 | function parseMcpContext(url: URL): { |
| 114 | − workspaceId: string; | |
| 123 | + workspaceId: Id<"workspaces">; | |
| 115 | 124 | clientId?: string; |
| 116 | 125 | sessionId?: string; |
| 117 | 126 | } | undefined { |
| 118 | − const workspaceId = url.searchParams.get("workspaceId"); | |
| 119 | − if (!workspaceId) return undefined; | |
| 127 | + const raw = url.searchParams.get("workspaceId"); | |
| 128 | + if (!raw) return undefined; | |
| 129 | + const workspaceId = parseWorkspaceId(raw); | |
| 120 | 130 | const clientId = url.searchParams.get("clientId") ?? undefined; |
| 121 | 131 | const sessionId = url.searchParams.get("sessionId") ?? undefined; |
| 122 | 132 | return { workspaceId, clientId, sessionId }; |
| @@ -162,7 +172,7 @@ const mcpHandler = httpAction(async (ctx, request) => { · 39 unmodified lines | ||
| 162 | 172 | try { |
| 163 | 173 | if (mcpAuthConfig.enabled && auth?.subject) { |
| 164 | 174 | const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForWorkosSubject, { |
| 165 | − workspaceId: requestedContext.workspaceId as Id<"workspaces">, | |
| 175 | + workspaceId: requestedContext.workspaceId, | |
| 166 | 176 | subject: auth.subject, |
| 167 | 177 | }); |
| 168 | 178 | |
| @@ -177,7 +187,7 @@ const mcpHandler = httpAction(async (ctx, request) => { · 8 unmodified lines | ||
| 177 | 187 | } |
| 178 | 188 | |
| 179 | 189 | const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForRequest, { |
| 180 | − workspaceId: requestedContext.workspaceId as Id<"workspaces">, | |
| 190 | + workspaceId: requestedContext.workspaceId, | |
| 181 | 191 | sessionId: requestedContext.sessionId, |
| 182 | 192 | }); |
| 183 | 193 | |
| @@ -209,18 +219,17 @@ const mcpHandler = httpAction(async (ctx, request) => { · 25 unmodified lines | ||
| 209 | 219 | timeoutMs?: number; |
| 210 | 220 | runtimeId?: string; |
| 211 | 221 | metadata?: Record<string, unknown>; |
| 212 | − workspaceId: string; | |
| 222 | + workspaceId: Id<"workspaces">; | |
| 213 | 223 | actorId: string; |
| 214 | 224 | clientId?: string; |
| 215 | 225 | }) => { |
| 216 | 226 | return (await ctx.runMutation(internal.executor.createTaskInternal, { |
| 217 | 227 | ...input, |
| 218 | − workspaceId: input.workspaceId as Id<"workspaces">, | |
| 219 | 228 | })) as { task: TaskRecord }; |
| 220 | 229 | }, |
| 221 | − getTask: async (taskId: string, workspaceId?: string) => { | |
| 230 | + getTask: async (taskId: string, workspaceId?: Id<"workspaces">) => { | |
| 222 | 231 | if (workspaceId) { |
| 223 | − return (await ctx.runQuery(internal.database.getTaskInWorkspace, { taskId, workspaceId: workspaceId as Id<"workspaces"> })) as TaskRecord | null; | |
| 232 | + return (await ctx.runQuery(internal.database.getTaskInWorkspace, { taskId, workspaceId })) as TaskRecord | null; | |
| 224 | 233 | } |
| 225 | 234 | return null; |
| 226 | 235 | }, |
| @@ -230,15 +239,15 @@ const mcpHandler = httpAction(async (ctx, request) => { · 3 unmodified lines | ||
| 230 | 239 | bootstrapAnonymousContext: async (sessionId?: string) => { |
| 231 | 240 | return (await ctx.runMutation(internal.database.bootstrapAnonymousSession, { sessionId })) as AnonymousContext; |
| 232 | 241 | }, |
| 233 | − listTools: async (toolContext?: { workspaceId: string; actorId?: string; clientId?: string }) => { | |
| 242 | + listTools: async (toolContext?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => { | |
| 234 | 243 | if (!toolContext) { |
| 235 | 244 | return []; |
| 236 | 245 | } |
| 237 | 246 | |
| 238 | − return (await ctx.runAction(internal.executorNode.listToolsInternal, { ...toolContext, workspaceId: toolContext.workspaceId as Id<"workspaces"> })) as ToolDescriptor[]; | |
| 247 | + return (await ctx.runAction(internal.executorNode.listToolsInternal, { ...toolContext })) as ToolDescriptor[]; | |
| 239 | 248 | }, |
| 240 | − listToolsForTypecheck: async (toolContext: { workspaceId: string; actorId?: string; clientId?: string }) => { | |
| 241 | − const result = await ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, { ...toolContext, workspaceId: toolContext.workspaceId as Id<"workspaces"> }) as { | |
| 249 | + listToolsForTypecheck: async (toolContext: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => { | |
| 250 | + const result = await ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, { ...toolContext }) as { | |
| 242 | 251 | tools: ToolDescriptor[]; |
| 243 | 252 | dtsUrls?: Record<string, string>; |
| 244 | 253 | }; |
| @@ -248,11 +257,11 @@ const mcpHandler = httpAction(async (ctx, request) => { · 3 unmodified lines | ||
| 248 | 257 | dtsUrls: result.dtsUrls ?? {}, |
| 249 | 258 | }; |
| 250 | 259 | }, |
| 251 | − listPendingApprovals: async (workspaceId: string) => { | |
| 252 | − return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId: workspaceId as Id<"workspaces"> })) as PendingApprovalRecord[]; | |
| 260 | + listPendingApprovals: async (workspaceId: Id<"workspaces">) => { | |
| 261 | + return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId })) as PendingApprovalRecord[]; | |
| 253 | 262 | }, |
| 254 | 263 | resolveApproval: async (input: { |
| 255 | − workspaceId: string; | |
| 264 | + workspaceId: Id<"workspaces">; | |
| 256 | 265 | approvalId: string; |
| 257 | 266 | decision: "approved" | "denied"; |
| 258 | 267 | reviewerId?: string; |
| @@ -260,7 +269,6 @@ const mcpHandler = httpAction(async (ctx, request) => { · 1 unmodified line | ||
| 260 | 269 | }) => { |
| 261 | 270 | return await ctx.runMutation(internal.executor.resolveApprovalInternal, { |
| 262 | 271 | ...input, |
| 263 | − workspaceId: input.workspaceId as Id<"workspaces">, | |
| 264 | 272 | }); |
| 265 | 273 | }, |
| 266 | 274 | }; |
| @@ -185,15 +185,12 @@ export const create = organizationMutation({ · 184 unmodified lines | ||
| 185 | 185 | } |
| 186 | 186 | const inviterWorkosUserId = ctx.account.providerAccountId; |
| 187 | 187 | |
| 188 | − const provider = "workos"; | |
| 189 | − | |
| 190 | 188 | const inviteId = await ctx.db.insert("invites", { |
| 191 | 189 | organizationId: ctx.organizationId, |
| 192 | 190 | workspaceId: args.workspaceId, |
| 193 | 191 | email: normalizedEmail, |
| 194 | 192 | role: args.role, |
| 195 | 193 | status: "pending", |
| 196 | − provider, | |
| 197 | 194 | invitedByAccountId: ctx.account._id, |
| 198 | 195 | expiresAt, |
| 199 | 196 | createdAt: now, |
| @@ -222,7 +219,6 @@ export const create = organizationMutation({ · 22 unmodified lines | ||
| 222 | 219 | expiresAt: invite.expiresAt, |
| 223 | 220 | }, |
| 224 | 221 | delivery: { |
| 225 | − provider, | |
| 226 | 222 | providerInviteId: invite.providerInviteId ?? null, |
| 227 | 223 | state: "queued", |
| 228 | 224 | }, |
| @@ -81,17 +81,6 @@ export const deleteSourceCredentialsMissingProvider = migrations.define({ · 80 unmodified lines | ||
| 81 | 81 | }, |
| 82 | 82 | }); |
| 83 | 83 | |
| 84 | −export const deleteInvitesMissingProvider = migrations.define({ | |
| 85 | − table: "invites", | |
| 86 | − migrateOne: async (ctx, invite) => { | |
| 87 | − if (invite.provider) { | |
| 88 | − return; | |
| 89 | − } | |
| 90 | − | |
| 91 | − await ctx.db.delete(invite._id); | |
| 92 | − }, | |
| 93 | −}); | |
| 94 | − | |
| 95 | 84 | export const deleteAnonymousSessionsMissingUserId = migrations.define({ |
| 96 | 85 | table: "anonymousSessions", |
| 97 | 86 | migrateOne: async (ctx, session) => { |
| @@ -112,4 +101,24 @@ export const backfillDtsStorageIds = migrations.define({ · 14 unmodified lines | ||
| 112 | 101 | }, |
| 113 | 102 | }); |
| 114 | 103 | |
| 104 | +export const cleanupTaskEmptyStringSentinels = migrations.define({ | |
| 105 | + table: "tasks", | |
| 106 | + migrateOne: async (_ctx, task) => { | |
| 107 | + const patch: Record<string, undefined> = {}; | |
| 108 | + if (task.actorId === "") patch.actorId = undefined; | |
| 109 | + if (task.clientId === "") patch.clientId = undefined; | |
| 110 | + if (Object.keys(patch).length > 0) return patch; | |
| 111 | + }, | |
| 112 | +}); | |
| 113 | + | |
| 114 | +export const cleanupAccessPolicyEmptyStringSentinels = migrations.define({ | |
| 115 | + table: "accessPolicies", | |
| 116 | + migrateOne: async (_ctx, policy) => { | |
| 117 | + const patch: Record<string, undefined> = {}; | |
| 118 | + if (policy.actorId === "") patch.actorId = undefined; | |
| 119 | + if (policy.clientId === "") patch.clientId = undefined; | |
| 120 | + if (Object.keys(patch).length > 0) return patch; | |
| 121 | + }, | |
| 122 | +}); | |
| 123 | + | |
| 115 | 124 | export const run = migrations.runner(); |
| @@ -66,35 +66,3 @@ export const putEntry = internalMutation({ · 65 unmodified lines | ||
| 66 | 66 | }); |
| 67 | 67 | }, |
| 68 | 68 | }); |
| 69 | − | |
| 70 | −/** | |
| 71 | − * Remove all cache entries older than the given timestamp. | |
| 72 | − * Returns the number of entries removed. | |
| 73 | − */ | |
| 74 | −export const pruneExpired = internalMutation({ | |
| 75 | − args: { | |
| 76 | − maxAgeMs: v.number(), | |
| 77 | − batchSize: v.optional(v.number()), | |
| 78 | − }, | |
| 79 | − handler: async (ctx, args) => { | |
| 80 | − const cutoff = Date.now() - args.maxAgeMs; | |
| 81 | − const limit = args.batchSize ?? 50; | |
| 82 | − | |
| 83 | − // Scan all entries (the table should be small — one row per unique spec URL) | |
| 84 | − const entries = await ctx.db | |
| 85 | − .query("openApiSpecCache") | |
| 86 | − .collect(); | |
| 87 | − | |
| 88 | − let removed = 0; | |
| 89 | − for (const entry of entries) { | |
| 90 | − if (removed >= limit) break; | |
| 91 | − if (entry.createdAt < cutoff) { | |
| 92 | − await ctx.storage.delete(entry.storageId).catch(() => {}); | |
| 93 | − await ctx.db.delete(entry._id); | |
| 94 | − removed++; | |
| 95 | − } | |
| 96 | − } | |
| 97 | − | |
| 98 | − return removed; | |
| 99 | − }, | |
| 100 | −}); | |
| @@ -75,7 +75,6 @@ export const create = authedMutation({ · 74 unmodified lines | ||
| 75 | 75 | organizationId, |
| 76 | 76 | slug: "default", |
| 77 | 77 | name: "Default Workspace", |
| 78 | − plan: "free", | |
| 79 | 78 | createdByAccountId: account._id, |
| 80 | 79 | createdAt: now, |
| 81 | 80 | updatedAt: now, |
| @@ -25,7 +25,6 @@ const inviteStatus = v.union( · 24 unmodified lines | ||
| 25 | 25 | v.literal("revoked"), |
| 26 | 26 | v.literal("failed"), |
| 27 | 27 | ); |
| 28 | −const inviteProvider = v.literal("workos"); | |
| 29 | 28 | const taskStatus = v.union( |
| 30 | 29 | v.literal("queued"), |
| 31 | 30 | v.literal("running"), |
| @@ -58,8 +57,7 @@ export default defineSchema({ · 26 unmodified lines | ||
| 58 | 57 | updatedAt: v.number(), |
| 59 | 58 | lastLoginAt: v.optional(v.number()), |
| 60 | 59 | }) |
| 61 | − .index("by_provider", ["provider", "providerAccountId"]) | |
| 62 | − .index("by_email", ["email"]), | |
| 60 | + .index("by_provider", ["provider", "providerAccountId"]), | |
| 63 | 61 | |
| 64 | 62 | workspaces: defineTable({ |
| 65 | 63 | workosOrgId: v.optional(v.string()), |
| @@ -67,7 +65,6 @@ export default defineSchema({ · 1 unmodified line | ||
| 67 | 65 | slug: v.string(), |
| 68 | 66 | name: v.string(), |
| 69 | 67 | iconStorageId: v.optional(v.id("_storage")), |
| 70 | − plan: v.string(), | |
| 71 | 68 | createdByAccountId: v.optional(v.id("accounts")), |
| 72 | 69 | createdAt: v.number(), |
| 73 | 70 | updatedAt: v.number(), |
| @@ -75,7 +72,6 @@ export default defineSchema({ · 1 unmodified line | ||
| 75 | 72 | .index("by_workos_org_id", ["workosOrgId"]) |
| 76 | 73 | .index("by_organization_created", ["organizationId", "createdAt"]) |
| 77 | 74 | .index("by_organization_slug", ["organizationId", "slug"]) |
| 78 | − .index("by_creator_created", ["createdByAccountId", "createdAt"]) | |
| 79 | 75 | .index("by_slug", ["slug"]), |
| 80 | 76 | |
| 81 | 77 | organizations: defineTable({ |
| @@ -106,7 +102,6 @@ export default defineSchema({ · 24 unmodified lines | ||
| 106 | 102 | .index("by_org", ["organizationId"]) |
| 107 | 103 | .index("by_org_account", ["organizationId", "accountId"]) |
| 108 | 104 | .index("by_account", ["accountId"]) |
| 109 | − .index("by_org_status", ["organizationId", "status"]) | |
| 110 | 105 | .index("by_org_billable_status", ["organizationId", "billable", "status"]), |
| 111 | 106 | |
| 112 | 107 | workspaceMembers: defineTable({ |
| @@ -121,7 +116,6 @@ export default defineSchema({ · 8 unmodified lines | ||
| 121 | 116 | .index("by_workspace", ["workspaceId"]) |
| 122 | 117 | .index("by_workspace_account", ["workspaceId", "accountId"]) |
| 123 | 118 | .index("by_account", ["accountId"]) |
| 124 | − .index("by_workspace_status", ["workspaceId", "status"]) | |
| 125 | 119 | .index("by_workos_membership_id", ["workosOrgMembershipId"]), |
| 126 | 120 | |
| 127 | 121 | invites: defineTable({ |
| @@ -130,7 +124,6 @@ export default defineSchema({ · 2 unmodified lines | ||
| 130 | 124 | email: v.string(), |
| 131 | 125 | role: orgRole, |
| 132 | 126 | status: inviteStatus, |
| 133 | − provider: inviteProvider, | |
| 134 | 127 | providerInviteId: v.optional(v.string()), |
| 135 | 128 | invitedByAccountId: v.id("accounts"), |
| 136 | 129 | expiresAt: v.number(), |
| @@ -139,7 +132,6 @@ export default defineSchema({ · 2 unmodified lines | ||
| 139 | 132 | updatedAt: v.number(), |
| 140 | 133 | }) |
| 141 | 134 | .index("by_org", ["organizationId"]) |
| 142 | − .index("by_org_status_created", ["organizationId", "status", "createdAt"]) | |
| 143 | 135 | .index("by_org_email_status", ["organizationId", "email", "status"]), |
| 144 | 136 | |
| 145 | 137 | billingCustomers: defineTable({ |
| @@ -182,8 +174,8 @@ export default defineSchema({ · 36 unmodified lines | ||
| 182 | 174 | code: v.string(), |
| 183 | 175 | runtimeId: v.string(), |
| 184 | 176 | workspaceId: v.id("workspaces"), |
| 185 | − actorId: v.string(), | |
| 186 | − clientId: v.string(), | |
| 177 | + actorId: v.optional(v.string()), | |
| 178 | + clientId: v.optional(v.string()), | |
| 187 | 179 | status: taskStatus, |
| 188 | 180 | timeoutMs: v.number(), |
| 189 | 181 | metadata: v.any(), |
| @@ -213,7 +205,6 @@ export default defineSchema({ · 23 unmodified lines | ||
| 213 | 205 | resolvedAt: v.optional(v.number()), |
| 214 | 206 | }) |
| 215 | 207 | .index("by_approval_id", ["approvalId"]) |
| 216 | − .index("by_task", ["taskId"]) | |
| 217 | 208 | .index("by_workspace_created", ["workspaceId", "createdAt"]) |
| 218 | 209 | .index("by_workspace_status_created", ["workspaceId", "status", "createdAt"]), |
| 219 | 210 | |
| @@ -225,14 +216,13 @@ export default defineSchema({ · 5 unmodified lines | ||
| 225 | 216 | payload: v.any(), |
| 226 | 217 | createdAt: v.number(), |
| 227 | 218 | }) |
| 228 | − .index("by_sequence", ["sequence"]) | |
| 229 | 219 | .index("by_task_sequence", ["taskId", "sequence"]), |
| 230 | 220 | |
| 231 | 221 | accessPolicies: defineTable({ |
| 232 | 222 | policyId: v.string(), |
| 233 | 223 | workspaceId: v.id("workspaces"), |
| 234 | − actorId: v.string(), | |
| 235 | − clientId: v.string(), | |
| 224 | + actorId: v.optional(v.string()), | |
| 225 | + clientId: v.optional(v.string()), | |
| 236 | 226 | toolPathPattern: v.string(), |
| 237 | 227 | decision: policyDecision, |
| 238 | 228 | priority: v.number(), |
| @@ -253,7 +243,6 @@ export default defineSchema({ · 14 unmodified lines | ||
| 253 | 243 | createdAt: v.number(), |
| 254 | 244 | updatedAt: v.number(), |
| 255 | 245 | }) |
| 256 | − .index("by_credential_id", ["credentialId"]) | |
| 257 | 246 | .index("by_workspace_created", ["workspaceId", "createdAt"]) |
| 258 | 247 | .index("by_workspace_source_scope_actor", ["workspaceId", "sourceKey", "scope", "actorId"]), |
| 259 | 248 | |
| @@ -269,8 +258,7 @@ export default defineSchema({ · 9 unmodified lines | ||
| 269 | 258 | }) |
| 270 | 259 | .index("by_source_id", ["sourceId"]) |
| 271 | 260 | .index("by_workspace_updated", ["workspaceId", "updatedAt"]) |
| 272 | − .index("by_workspace_name", ["workspaceId", "name"]) | |
| 273 | − .index("by_workspace_enabled_updated", ["workspaceId", "enabled", "updatedAt"]), | |
| 261 | + .index("by_workspace_name", ["workspaceId", "name"]), | |
| 274 | 262 | |
| 275 | 263 | agentTasks: defineTable({ |
| 276 | 264 | agentTaskId: v.string(), |
| @@ -286,8 +274,7 @@ export default defineSchema({ · 9 unmodified lines | ||
| 286 | 274 | updatedAt: v.number(), |
| 287 | 275 | }) |
| 288 | 276 | .index("by_agent_task_id", ["agentTaskId"]) |
| 289 | − .index("by_workspace_created", ["workspaceId", "createdAt"]) | |
| 290 | − .index("by_requester_created", ["requesterId", "createdAt"]), | |
| 277 | + .index("by_workspace_created", ["workspaceId", "createdAt"]), | |
| 291 | 278 | |
| 292 | 279 | openApiSpecCache: defineTable({ |
| 293 | 280 | specUrl: v.string(), |
| @@ -2,6 +2,7 @@ import { v } from "convex/values"; · 1 unmodified line | ||
| 2 | 2 | import { internal } from "./_generated/api"; |
| 3 | 3 | import { mutation, query } from "./_generated/server"; |
| 4 | 4 | import { workspaceMutation, workspaceQuery } from "../lib/functionBuilders"; |
| 5 | +import { actorIdForAccount } from "../lib/identity"; | |
| 5 | 6 | |
| 6 | 7 | const approvalStatusValidator = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied")); |
| 7 | 8 | const policyDecisionValidator = v.union(v.literal("allow"), v.literal("require_approval"), v.literal("deny")); |
| @@ -9,10 +10,6 @@ const credentialScopeValidator = v.union(v.literal("workspace"), v.literal("acto · 1 unmodified line | ||
| 9 | 10 | const credentialProviderValidator = v.union(v.literal("managed"), v.literal("workos-vault")); |
| 10 | 11 | const toolSourceTypeValidator = v.union(v.literal("mcp"), v.literal("openapi"), v.literal("graphql")); |
| 11 | 12 | |
| 12 | −function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }): string { | |
| 13 | − return account.provider === "anonymous" ? account.providerAccountId : account._id; | |
| 14 | −} | |
| 15 | − | |
| 16 | 13 | function redactCredential<T extends { secretJson: Record<string, unknown> }>(credential: T): T { |
| 17 | 14 | return { |
| 18 | 15 | ...credential, |
| @@ -1,15 +1,12 @@ | ||
| 1 | 1 | import { v } from "convex/values"; |
| 2 | 2 | import { internalQuery } from "./_generated/server"; |
| 3 | 3 | import { |
| 4 | + actorIdForAccount, | |
| 4 | 5 | requireWorkspaceAccessForAccount, |
| 5 | 6 | resolveAccountForRequest, |
| 6 | 7 | resolveWorkosAccountBySubject, |
| 7 | 8 | } from "../lib/identity"; |
| 8 | 9 | |
| 9 | −function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }): string { | |
| 10 | − return account.provider === "anonymous" ? account.providerAccountId : account._id; | |
| 11 | −} | |
| 12 | − | |
| 13 | 10 | export const getWorkspaceAccessForRequest = internalQuery({ |
| 14 | 11 | args: { |
| 15 | 12 | workspaceId: v.id("workspaces"), |
| @@ -72,21 +72,3 @@ export const putEntry = internalMutation({ · 71 unmodified lines | ||
| 72 | 72 | }); |
| 73 | 73 | }, |
| 74 | 74 | }); |
| 75 | − | |
| 76 | −/** | |
| 77 | − * Look up .d.ts storage IDs for a workspace (used to generate download URLs). | |
| 78 | − */ | |
| 79 | −export const getDtsStorageIds = internalQuery({ | |
| 80 | − args: { | |
| 81 | − workspaceId: v.id("workspaces"), | |
| 82 | − }, | |
| 83 | − handler: async (ctx, args) => { | |
| 84 | − const entry = await ctx.db | |
| 85 | − .query("workspaceToolCache") | |
| 86 | − .withIndex("by_workspace", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 87 | − .unique(); | |
| 88 | − | |
| 89 | − if (!entry) return []; | |
| 90 | − return entry.dtsStorageIds; | |
| 91 | − }, | |
| 92 | −}); | |
| @@ -193,7 +193,6 @@ export const create = authedMutation({ · 192 unmodified lines | ||
| 193 | 193 | slug, |
| 194 | 194 | name, |
| 195 | 195 | iconStorageId: args.iconStorageId, |
| 196 | − plan: "free", | |
| 197 | 196 | createdByAccountId: account._id, |
| 198 | 197 | createdAt: now, |
| 199 | 198 | updatedAt: now, |
| @@ -158,3 +158,7 @@ export function isAdminRole(role: string): boolean { · 157 unmodified lines | ||
| 158 | 158 | export function canManageBilling(role: string): boolean { |
| 159 | 159 | return role === "owner" || role === "billing_admin"; |
| 160 | 160 | } |
| 161 | + | |
| 162 | +export function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }): string { | |
| 163 | + return account.provider === "anonymous" ? account.providerAccountId : account._id; | |
| 164 | +} | |
| @@ -3,6 +3,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; · 2 unmodified lines | ||
| 3 | 3 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; |
| 4 | 4 | import { handleMcpRequest } from "./mcp_server"; |
| 5 | 5 | import type { LiveTaskEvent } from "./events"; |
| 6 | +import type { Id } from "../convex/_generated/dataModel"; | |
| 6 | 7 | import type { AnonymousContext, CreateTaskInput, TaskRecord, ToolDescriptor } from "./types"; |
| 7 | 8 | |
| 8 | 9 | class FakeMcpService { |
| @@ -75,7 +76,7 @@ class FakeMcpService { · 66 unmodified lines | ||
| 75 | 76 | const now = Date.now(); |
| 76 | 77 | const context: AnonymousContext = { |
| 77 | 78 | sessionId: sessionId ?? `anon_session_${crypto.randomUUID()}`, |
| 78 | − workspaceId: `ws_${crypto.randomUUID()}`, | |
| 79 | + workspaceId: `ws_${crypto.randomUUID()}` as Id<"workspaces">, | |
| 79 | 80 | actorId: `anon_${crypto.randomUUID()}`, |
| 80 | 81 | clientId: "mcp", |
| 81 | 82 | accountId: `account_${crypto.randomUUID()}`, |
| @@ -10,6 +10,7 @@ import type { · 9 unmodified lines | ||
| 10 | 10 | TaskRecord, |
| 11 | 11 | ToolDescriptor, |
| 12 | 12 | } from "./types"; |
| 13 | +import type { Id } from "../convex/_generated/dataModel"; | |
| 13 | 14 | |
| 14 | 15 | function getTaskTerminalState(status: string): boolean { |
| 15 | 16 | return status === "completed" || status === "failed" || status === "timed_out" || status === "denied"; |
| @@ -21,16 +22,16 @@ function getTaskTerminalState(status: string): boolean { · 5 unmodified lines | ||
| 21 | 22 | |
| 22 | 23 | interface McpExecutorService { |
| 23 | 24 | createTask(input: CreateTaskInput): Promise<{ task: TaskRecord }>; |
| 24 | − getTask(taskId: string, workspaceId?: string): Promise<TaskRecord | null>; | |
| 25 | + getTask(taskId: string, workspaceId?: Id<"workspaces">): Promise<TaskRecord | null>; | |
| 25 | 26 | subscribe(taskId: string, listener: (event: LiveTaskEvent) => void): () => void; |
| 26 | 27 | bootstrapAnonymousContext(sessionId?: string): Promise<AnonymousContext>; |
| 27 | − listTools(context?: { workspaceId: string; actorId?: string; clientId?: string }): Promise<ToolDescriptor[]>; | |
| 28 | + listTools(context?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }): Promise<ToolDescriptor[]>; | |
| 28 | 29 | listToolsForTypecheck?( |
| 29 | − context: { workspaceId: string; actorId?: string; clientId?: string }, | |
| 30 | + context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }, | |
| 30 | 31 | ): Promise<{ tools: ToolDescriptor[]; dtsUrls: Record<string, string> }>; |
| 31 | − listPendingApprovals?(workspaceId: string): Promise<PendingApprovalRecord[]>; | |
| 32 | + listPendingApprovals?(workspaceId: Id<"workspaces">): Promise<PendingApprovalRecord[]>; | |
| 32 | 33 | resolveApproval?(input: { |
| 33 | − workspaceId: string; | |
| 34 | + workspaceId: Id<"workspaces">; | |
| 34 | 35 | approvalId: string; |
| 35 | 36 | decision: "approved" | "denied"; |
| 36 | 37 | reviewerId?: string; |
| @@ -44,7 +45,7 @@ interface ApprovalPromptDecision { · 7 unmodified lines | ||
| 44 | 45 | } |
| 45 | 46 | |
| 46 | 47 | interface ApprovalPromptContext { |
| 47 | − workspaceId: string; | |
| 48 | + workspaceId: Id<"workspaces">; | |
| 48 | 49 | actorId: string; |
| 49 | 50 | } |
| 50 | 51 | |
| @@ -58,7 +59,7 @@ type ApprovalPrompt = ( · 7 unmodified lines | ||
| 58 | 59 | // --------------------------------------------------------------------------- |
| 59 | 60 | |
| 60 | 61 | export interface McpWorkspaceContext { |
| 61 | − workspaceId: string; | |
| 62 | + workspaceId: Id<"workspaces">; | |
| 62 | 63 | actorId: string; |
| 63 | 64 | clientId?: string; |
| 64 | 65 | sessionId?: string; |
| @@ -173,7 +174,7 @@ function summarizeTask(task: TaskRecord): string { · 108 unmodified lines | ||
| 173 | 174 | function waitForTerminalTask( |
| 174 | 175 | service: McpExecutorService, |
| 175 | 176 | taskId: string, |
| 176 | − workspaceId: string, | |
| 177 | + workspaceId: Id<"workspaces">, | |
| 177 | 178 | waitTimeoutMs: number, |
| 178 | 179 | onApprovalPrompt?: ApprovalPrompt, |
| 179 | 180 | approvalContext?: ApprovalPromptContext, |
| @@ -422,7 +423,7 @@ function createRunCodeTool( · 242 unmodified lines | ||
| 422 | 423 | const requestedTimeoutMs = input.timeoutMs ?? 300_000; |
| 423 | 424 | |
| 424 | 425 | // Resolve context: bound context takes priority, then input, then anonymous |
| 425 | − let context: { workspaceId: string; actorId: string; clientId?: string; sessionId?: string }; | |
| 426 | + let context: { workspaceId: Id<"workspaces">; actorId: string; clientId?: string; sessionId?: string }; | |
| 426 | 427 | |
| 427 | 428 | if (boundContext) { |
| 428 | 429 | context = { ...boundContext, sessionId: input.sessionId ?? boundContext.sessionId }; |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | 2 | import { runCodeWithAdapter } from "./runtime_core"; |
| 3 | 3 | import { loadExternalTools } from "../tool_sources"; |
| 4 | +import type { Id } from "../../convex/_generated/dataModel"; | |
| 4 | 5 | import type { |
| 5 | 6 | ExecutionAdapter, |
| 6 | 7 | RuntimeOutputEvent, |
| @@ -34,7 +35,7 @@ function createRuntimeAdapter( · 27 unmodified lines | ||
| 34 | 35 | try { |
| 35 | 36 | const context: ToolRunContext = { |
| 36 | 37 | taskId: call.runId, |
| 37 | − workspaceId: "ws_test", | |
| 38 | + workspaceId: "ws_test" as Id<"workspaces">, | |
| 38 | 39 | actorId: "actor_test", |
| 39 | 40 | clientId: "web", |
| 40 | 41 | isToolAllowed: () => true, |
| @@ -1,7 +1,10 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | +import type { Id } from "../convex/_generated/dataModel"; | |
| 2 | 3 | import { createDiscoverTool } from "./tool_discovery"; |
| 3 | 4 | import type { ToolDefinition } from "./types"; |
| 4 | 5 | |
| 6 | +const TEST_WORKSPACE_ID = "w" as Id<"workspaces">; | |
| 7 | + | |
| 5 | 8 | test("discover returns aliases and example calls", async () => { |
| 6 | 9 | const tool = createDiscoverTool([ |
| 7 | 10 | { |
| @@ -19,7 +22,7 @@ test("discover returns aliases and example calls", async () => { · 11 unmodified lines | ||
| 19 | 22 | |
| 20 | 23 | const result = await tool.run( |
| 21 | 24 | { query: "addnumbers", depth: 2 }, |
| 22 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 25 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 23 | 26 | ) as { |
| 24 | 27 | bestPath: string | null; |
| 25 | 28 | results: Array<{ |
| @@ -57,7 +60,7 @@ test("discover example call handles input-shaped args", async () => { · 31 unmodified lines | ||
| 57 | 60 | |
| 58 | 61 | const result = await tool.run( |
| 59 | 62 | { query: "issuecreate", depth: 2 }, |
| 60 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 63 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 61 | 64 | ) as { |
| 62 | 65 | bestPath: string | null; |
| 63 | 66 | results: Array<{ exampleCall: string }>; |
| @@ -86,7 +89,7 @@ test("discover uses compact signatures by default and allows full mode", async ( · 22 unmodified lines | ||
| 86 | 89 | |
| 87 | 90 | const compactResult = await tool.run( |
| 88 | 91 | { query: "linear teams", depth: 2 }, |
| 89 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 92 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 90 | 93 | ) as { |
| 91 | 94 | bestPath: string | null; |
| 92 | 95 | results: Array<{ description: string; signature: string }>; |
| @@ -94,7 +97,7 @@ test("discover uses compact signatures by default and allows full mode", async ( · 1 unmodified line | ||
| 94 | 97 | |
| 95 | 98 | const fullResult = await tool.run( |
| 96 | 99 | { query: "linear teams", depth: 2, compact: false }, |
| 97 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 100 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 98 | 101 | ) as { |
| 99 | 102 | bestPath: string | null; |
| 100 | 103 | results: Array<{ description: string; signature: string }>; |
| @@ -128,7 +131,7 @@ test("discover returns null bestPath when there are no matches", async () => { · 27 unmodified lines | ||
| 128 | 131 | |
| 129 | 132 | const result = await tool.run( |
| 130 | 133 | { query: "totally_unrelated_keyword" }, |
| 131 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 134 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 132 | 135 | ) as { bestPath: string | null; results: Array<unknown>; total: number }; |
| 133 | 136 | |
| 134 | 137 | expect(result.bestPath).toBeNull(); |
| @@ -164,7 +167,7 @@ test("discover bestPath prefers simpler exact intent operation", async () => { · 29 unmodified lines | ||
| 164 | 167 | |
| 165 | 168 | const result = await tool.run( |
| 166 | 169 | { query: "linear issue create", depth: 2 }, |
| 167 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 170 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 168 | 171 | ) as { bestPath: string | null; results: Array<{ path: string }> }; |
| 169 | 172 | |
| 170 | 173 | expect(result.bestPath).toBe("linear.mutation.issuecreate"); |
| @@ -199,7 +202,7 @@ test("discover namespace hint suppresses cross-namespace bestPath", async () => · 28 unmodified lines | ||
| 199 | 202 | |
| 200 | 203 | const result = await tool.run( |
| 201 | 204 | { query: "linear teams list", depth: 2 }, |
| 202 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 205 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 203 | 206 | ) as { bestPath: string | null; results: Array<{ path: string }> }; |
| 204 | 207 | |
| 205 | 208 | expect(result.bestPath).toBe("linear.query.teams"); |
| @@ -1,7 +1,10 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | +import type { Id } from "../convex/_generated/dataModel"; | |
| 2 | 3 | import { loadExternalTools } from "./tool_sources"; |
| 3 | 4 | import type { ExternalToolSourceConfig } from "./tool_sources"; |
| 4 | 5 | |
| 6 | +const TEST_WORKSPACE_ID = "w" as Id<"workspaces">; | |
| 7 | + | |
| 5 | 8 | function makeInlineSpec(tag: string, operationId: string): Record<string, unknown> { |
| 6 | 9 | return { |
| 7 | 10 | openapi: "3.0.3", |
| @@ -461,7 +464,7 @@ test("graphql helper tools generate valid selection sets and envelope responses" · 453 unmodified lines | ||
| 461 | 464 | expect(batchTool).toBeDefined(); |
| 462 | 465 | expect(rawTool).toBeDefined(); |
| 463 | 466 | |
| 464 | − const context = { taskId: "t", workspaceId: "w", isToolAllowed: () => true }; | |
| 467 | + const context = { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }; | |
| 465 | 468 | |
| 466 | 469 | const teamsResult = await teamsTool!.run({}, context); |
| 467 | 470 | const batchResult = await batchTool!.run({ input: { issues: ["one"] } }, context); |
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | // ── Shared types (inlined from @executor/contracts) ────────────────────────── |
| 2 | 2 | |
| 3 | +import type { Id } from "../convex/_generated/dataModel"; | |
| 4 | + | |
| 3 | 5 | export type TaskStatus = "queued" | "running" | "completed" | "failed" | "timed_out" | "denied"; |
| 4 | 6 | export type ApprovalStatus = "pending" | "approved" | "denied"; |
| 5 | 7 | export type PolicyDecision = "allow" | "require_approval" | "deny"; |
| @@ -15,7 +17,7 @@ export interface TaskRecord { · 9 unmodified lines | ||
| 15 | 17 | status: TaskStatus; |
| 16 | 18 | timeoutMs: number; |
| 17 | 19 | metadata: Record<string, unknown>; |
| 18 | − workspaceId: string; | |
| 20 | + workspaceId: Id<"workspaces">; | |
| 19 | 21 | actorId?: string; |
| 20 | 22 | clientId?: string; |
| 21 | 23 | createdAt: number; |
| @@ -55,7 +57,7 @@ export interface TaskEventRecord { · 33 unmodified lines | ||
| 55 | 57 | |
| 56 | 58 | export interface AccessPolicyRecord { |
| 57 | 59 | id: string; |
| 58 | − workspaceId: string; | |
| 60 | + workspaceId: Id<"workspaces">; | |
| 59 | 61 | actorId?: string; |
| 60 | 62 | clientId?: string; |
| 61 | 63 | toolPathPattern: string; |
| @@ -67,7 +69,7 @@ export interface AccessPolicyRecord { · 5 unmodified lines | ||
| 67 | 69 | |
| 68 | 70 | export interface CredentialRecord { |
| 69 | 71 | id: string; |
| 70 | − workspaceId: string; | |
| 72 | + workspaceId: Id<"workspaces">; | |
| 71 | 73 | sourceKey: string; |
| 72 | 74 | scope: CredentialScope; |
| 73 | 75 | actorId?: string; |
| @@ -79,7 +81,7 @@ export interface CredentialRecord { · 5 unmodified lines | ||
| 79 | 81 | |
| 80 | 82 | export interface ToolSourceRecord { |
| 81 | 83 | id: string; |
| 82 | − workspaceId: string; | |
| 84 | + workspaceId: Id<"workspaces">; | |
| 83 | 85 | name: string; |
| 84 | 86 | type: ToolSourceType; |
| 85 | 87 | config: Record<string, unknown>; |
| @@ -112,7 +114,7 @@ export interface OpenApiSourceQuality { · 26 unmodified lines | ||
| 112 | 114 | |
| 113 | 115 | export interface AnonymousContext { |
| 114 | 116 | sessionId: string; |
| 115 | − workspaceId: string; | |
| 117 | + workspaceId: Id<"workspaces">; | |
| 116 | 118 | actorId: string; |
| 117 | 119 | clientId: string; |
| 118 | 120 | accountId: string; |
| @@ -128,7 +130,7 @@ export interface CreateTaskInput { · 9 unmodified lines | ||
| 128 | 130 | timeoutMs?: number; |
| 129 | 131 | runtimeId?: string; |
| 130 | 132 | metadata?: Record<string, unknown>; |
| 131 | − workspaceId: string; | |
| 133 | + workspaceId: Id<"workspaces">; | |
| 132 | 134 | actorId: string; |
| 133 | 135 | clientId?: string; |
| 134 | 136 | } |
| @@ -201,7 +203,7 @@ export interface ResolvedToolCredential { · 66 unmodified lines | ||
| 201 | 203 | |
| 202 | 204 | export interface ToolRunContext { |
| 203 | 205 | taskId: string; |
| 204 | − workspaceId: string; | |
| 206 | + workspaceId: Id<"workspaces">; | |
| 205 | 207 | actorId?: string; |
| 206 | 208 | clientId?: string; |
| 207 | 209 | credential?: ResolvedToolCredential; |
| @@ -16,6 +16,8 @@ import { · 15 unmodified lines | ||
| 16 | 16 | } from "./tool_sources"; |
| 17 | 17 | import type { ToolDefinition } from "./types"; |
| 18 | 18 | |
| 19 | +const TEST_WORKSPACE_ID = "w" as Id<"workspaces">; | |
| 20 | + | |
| 19 | 21 | function makeBaseTools(): Map<string, ToolDefinition> { |
| 20 | 22 | return new Map([ |
| 21 | 23 | [ |
| @@ -236,7 +238,7 @@ describe("serializeTools + rehydrateTools round-trip", () => { · 214 unmodified lines | ||
| 236 | 238 | // The rehydrated run should work |
| 237 | 239 | const result = await rehydrated[0]!.run( |
| 238 | 240 | { message: "hello" }, |
| 239 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 241 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 240 | 242 | ); |
| 241 | 243 | expect(result).toBe("hello"); |
| 242 | 244 | }); |
| @@ -344,11 +346,11 @@ describe("serializeTools + rehydrateTools round-trip", () => { · 101 unmodified lines | ||
| 344 | 346 | try { |
| 345 | 347 | const rawResult = await rawTool!.run( |
| 346 | 348 | { query: "query viewer { viewer { id } }" }, |
| 347 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 349 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 348 | 350 | ); |
| 349 | 351 | const teamsResult = await teamsTool!.run( |
| 350 | 352 | {}, |
| 351 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 353 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 352 | 354 | ); |
| 353 | 355 | |
| 354 | 356 | expect(rawResult).toEqual({ data: { viewer: { id: "user_1" } }, errors: [] }); |
| @@ -395,7 +397,7 @@ describe("serializeTools + rehydrateTools round-trip", () => { · 40 unmodified lines | ||
| 395 | 397 | const echoTool = rehydrated.find((t) => t.path === "echo")!; |
| 396 | 398 | const result = await echoTool.run( |
| 397 | 399 | { message: "round-trip" }, |
| 398 | − { taskId: "t", workspaceId: "w", isToolAllowed: () => true }, | |
| 400 | + { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | |
| 399 | 401 | ); |
| 400 | 402 | expect(result).toBe("round-trip"); |
| 401 | 403 | |
| @@ -487,7 +489,6 @@ describe("workspace tool cache table", () => { · 85 unmodified lines | ||
| 487 | 489 | name: "test-ws", |
| 488 | 490 | slug: "test-ws", |
| 489 | 491 | organizationId: orgId, |
| 490 | − plan: "free", | |
| 491 | 492 | createdAt: Date.now(), |
| 492 | 493 | updatedAt: Date.now(), |
| 493 | 494 | }); |
| @@ -94,13 +94,23 @@ async function verifyOptionalMcpToken(request: Request): Promise<string | null> · 93 unmodified lines | ||
| 94 | 94 | return payload.sub; |
| 95 | 95 | } |
| 96 | 96 | |
| 97 | +/** | |
| 98 | + * Validated boundary for external workspace ID strings. | |
| 99 | + * Convex IDs are opaque strings at runtime; this cast is the single | |
| 100 | + * validated entry point so downstream code never needs ad-hoc casts. | |
| 101 | + */ | |
| 102 | +function parseWorkspaceId(raw: string): Id<"workspaces"> { | |
| 103 | + return raw as Id<"workspaces">; | |
| 104 | +} | |
| 105 | + | |
| 97 | 106 | function parseRequestedContext(url: URL): { |
| 98 | − workspaceId?: string; | |
| 107 | + workspaceId?: Id<"workspaces">; | |
| 99 | 108 | actorId?: string; |
| 100 | 109 | clientId?: string; |
| 101 | 110 | sessionId?: string; |
| 102 | 111 | } { |
| 103 | − const workspaceId = url.searchParams.get("workspaceId") ?? undefined; | |
| 112 | + const rawWorkspaceId = url.searchParams.get("workspaceId"); | |
| 113 | + const workspaceId = rawWorkspaceId ? parseWorkspaceId(rawWorkspaceId) : undefined; | |
| 104 | 114 | const actorId = url.searchParams.get("actorId") ?? undefined; |
| 105 | 115 | const clientId = url.searchParams.get("clientId") ?? undefined; |
| 106 | 116 | const sessionId = url.searchParams.get("sessionId") ?? undefined; |
| @@ -122,12 +132,12 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 15 unmodified lines | ||
| 122 | 132 | timeoutMs?: number; |
| 123 | 133 | runtimeId?: string; |
| 124 | 134 | metadata?: Record<string, unknown>; |
| 125 | − workspaceId: string; | |
| 135 | + workspaceId: Id<"workspaces">; | |
| 126 | 136 | actorId: string; |
| 127 | 137 | clientId?: string; |
| 128 | 138 | }) => { |
| 129 | 139 | const created = await convex.mutation(api.executor.createTask, { |
| 130 | − workspaceId: input.workspaceId as Id<"workspaces">, | |
| 140 | + workspaceId: input.workspaceId, | |
| 131 | 141 | sessionId, |
| 132 | 142 | code: input.code, |
| 133 | 143 | timeoutMs: input.timeoutMs, |
| @@ -139,13 +149,13 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 5 unmodified lines | ||
| 139 | 149 | return created as { task: TaskRecord }; |
| 140 | 150 | }, |
| 141 | 151 | |
| 142 | − getTask: async (taskId: string, workspace?: string) => { | |
| 152 | + getTask: async (taskId: string, workspace?: Id<"workspaces">) => { | |
| 143 | 153 | const effectiveWorkspaceId = workspace ?? workspaceId; |
| 144 | 154 | if (!effectiveWorkspaceId) { |
| 145 | 155 | return null; |
| 146 | 156 | } |
| 147 | 157 | const task = await convex.query(api.workspace.getTaskInWorkspace, { |
| 148 | − workspaceId: effectiveWorkspaceId as Id<"workspaces">, | |
| 158 | + workspaceId: effectiveWorkspaceId, | |
| 149 | 159 | sessionId, |
| 150 | 160 | taskId, |
| 151 | 161 | }); |
| @@ -161,12 +171,12 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 9 unmodified lines | ||
| 161 | 171 | return bootstrap as AnonymousContext; |
| 162 | 172 | }, |
| 163 | 173 | |
| 164 | − listTools: async (toolContext?: { workspaceId: string; actorId?: string; clientId?: string }) => { | |
| 174 | + listTools: async (toolContext?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => { | |
| 165 | 175 | if (!toolContext) { |
| 166 | 176 | return []; |
| 167 | 177 | } |
| 168 | 178 | const tools = await convex.action(api.executorNode.listTools, { |
| 169 | − workspaceId: toolContext.workspaceId as Id<"workspaces">, | |
| 179 | + workspaceId: toolContext.workspaceId, | |
| 170 | 180 | sessionId, |
| 171 | 181 | actorId: toolContext.actorId, |
| 172 | 182 | clientId: toolContext.clientId, |
| @@ -174,23 +184,23 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 1 unmodified line | ||
| 174 | 184 | return tools as ToolDescriptor[]; |
| 175 | 185 | }, |
| 176 | 186 | |
| 177 | − listPendingApprovals: async (approvalWorkspaceId: string) => { | |
| 187 | + listPendingApprovals: async (approvalWorkspaceId: Id<"workspaces">) => { | |
| 178 | 188 | const approvals = await convex.query(api.workspace.listPendingApprovals, { |
| 179 | − workspaceId: approvalWorkspaceId as Id<"workspaces">, | |
| 189 | + workspaceId: approvalWorkspaceId, | |
| 180 | 190 | sessionId, |
| 181 | 191 | }); |
| 182 | 192 | return approvals as PendingApprovalRecord[]; |
| 183 | 193 | }, |
| 184 | 194 | |
| 185 | 195 | resolveApproval: async (input: { |
| 186 | − workspaceId: string; | |
| 196 | + workspaceId: Id<"workspaces">; | |
| 187 | 197 | approvalId: string; |
| 188 | 198 | decision: "approved" | "denied"; |
| 189 | 199 | reviewerId?: string; |
| 190 | 200 | reason?: string; |
| 191 | 201 | }) => { |
| 192 | 202 | return await convex.mutation(api.executor.resolveApproval, { |
| 193 | − workspaceId: input.workspaceId as Id<"workspaces">, | |
| 203 | + workspaceId: input.workspaceId, | |
| 194 | 204 | sessionId, |
| 195 | 205 | approvalId: input.approvalId, |
| 196 | 206 | decision: input.decision, |
| @@ -203,7 +213,7 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 6 unmodified lines | ||
| 203 | 213 | |
| 204 | 214 | async function resolveContext( |
| 205 | 215 | requested: { |
| 206 | − workspaceId?: string; | |
| 216 | + workspaceId?: Id<"workspaces">; | |
| 207 | 217 | actorId?: string; |
| 208 | 218 | clientId?: string; |
| 209 | 219 | sessionId?: string; |
| @@ -233,7 +243,7 @@ async function resolveContext( · 23 unmodified lines | ||
| 233 | 243 | } |
| 234 | 244 | |
| 235 | 245 | const requestContext = await convex.query(api.workspace.getRequestContext, { |
| 236 | − workspaceId: requested.workspaceId as Id<"workspaces">, | |
| 246 | + workspaceId: requested.workspaceId, | |
| 237 | 247 | sessionId: requested.sessionId, |
| 238 | 248 | }); |
| 239 | 249 | |