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