History

Drop unused indexes, remove constant fields, finish workspaceId typing, fix empty-string sentinels, eliminate ctx:any and unsafe casts

- 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

Rhys Sullivan committed 9 months ago

Commit 2f609871 parent 1005f8333 files · +232 −255Browse files

Unified · Split

33 files
  1. assistant/packages/bot/src/commands/ask.tsx +3 −2
  2. assistant/packages/bot/src/views/task-message.tsx +2 −1
  3. assistant/packages/server/src/index.ts +3 −2
  4. assistant/packages/server/src/routes.ts +2 −1
  5. executor/apps/web/src/app/sign-in/route.ts +1 −1
  6. executor/apps/web/src/app/sign-up/route.ts +1 −1
  7. executor/apps/web/src/lib/session-context.tsx +2 −2
  8. executor/convex/_generated/api.d.ts +15 −15
  9. executor/convex/access-controls.test.ts +0 −1
  10. executor/convex/auth.ts +0 −2
  11. executor/convex/database.test.ts +0 −1
  12. executor/convex/database.ts +14 −30
  13. executor/convex/executor.ts +1 −4
  14. executor/convex/executorNode.ts +63 −35
  15. executor/convex/http.ts +25 −17
  16. executor/convex/invites.ts +0 −4
  17. executor/convex/migrations.ts +20 −11
  18. executor/convex/openApiSpecCache.ts +0 −32
  19. executor/convex/organizations.ts +0 −1
  20. executor/convex/schema.ts +7 −20
  21. executor/convex/workspace.ts +1 −4
  22. executor/convex/workspaceAuthInternal.ts +1 −4
  23. executor/convex/workspaceToolCache.ts +0 −18
  24. executor/convex/workspaces.ts +0 −1
  25. executor/lib/identity.ts +4 −0
  26. executor/lib/mcp-server.test.ts +2 −1
  27. executor/lib/mcp_server.ts +10 −9
  28. executor/lib/runtimes/runtime-core.test.ts +2 −1
  29. executor/lib/tool-discovery.test.ts +10 −7
  30. executor/lib/tool-sources.test.ts +4 −1
  31. executor/lib/types.ts +9 −7
  32. executor/lib/workspace-tool-cache.test.ts +6 −5
  33. executor/mcp-gateway.ts +24 −14
assistant/packages/bot/src/commands/ask.tsx+3−2
@@ -9,6 +9,7 @@ import type { ConvexReactClient } from "convex/react"; · 8 unmodified lines
99 import { ConvexProvider } from "convex/react";
1010 import type { ReacordInstance } from "@openassistant/reacord";
1111 import { Effect, Runtime } from "effect";
12+import type { Id } from "@executor/convex/_generated/dataModel";
1213 import { TaskMessage } from "../views/task-message";
1314
1415 interface AskCommandDeps {
@@ -29,13 +30,13 @@ export async function handleAskCommand( · 14 unmodified lines
2930 await interaction.deferReply();
3031
3132 let agentTaskId: string;
32− let workspaceId: string;
33+ let workspaceId: Id<"workspaces">;
3334 try {
3435 const data = await unwrap(
3536 deps.api.api.tasks.post({ prompt, requesterId }),
3637 );
3738 agentTaskId = data.agentTaskId;
38− workspaceId = data.workspaceId;
39+ workspaceId = data.workspaceId as Id<"workspaces">;
3940 } catch (error) {
4041 await interaction.editReply({
4142 content: `\u274c Failed to create task: ${error instanceof Error ? error.message : String(error)}`,
assistant/packages/bot/src/views/task-message.tsx+2−1
@@ -18,6 +18,7 @@ import { · 17 unmodified lines
1818 } from "@openassistant/reacord";
1919 import { useMutation, useQuery } from "convex/react";
2020 import { api } from "@executor/convex/_generated/api";
21+import type { Id } from "@executor/convex/_generated/dataModel";
2122
2223 // ---------------------------------------------------------------------------
2324 // Component
@@ -26,7 +27,7 @@ import { api } from "@executor/convex/_generated/api"; · 2 unmodified lines
2627 export interface TaskMessageProps {
2728 readonly agentTaskId: string;
2829 readonly prompt: string;
29− readonly workspaceId: string;
30+ readonly workspaceId: Id<"workspaces">;
3031 }
3132
3233 export function TaskMessage({ agentTaskId, prompt, workspaceId }: TaskMessageProps) {
assistant/packages/server/src/index.ts+3−2
@@ -5,6 +5,7 @@ · 4 unmodified lines
55 import { createApp } from "./routes";
66 import { ConvexHttpClient } from "convex/browser";
77 import { api } from "@executor/convex/_generated/api";
8+import type { Id } from "@executor/convex/_generated/dataModel";
89
910 const PORT = Number(Bun.env.PORT ?? 3000);
1011 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
2324
2425 // Bootstrap anonymous context on executor Convex backend
2526 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 };
2728
2829 if (PINNED_WORKSPACE_ID && PINNED_ACTOR_ID) {
2930 ctx = {
30− workspaceId: PINNED_WORKSPACE_ID,
31+ workspaceId: PINNED_WORKSPACE_ID as Id<"workspaces">,
3132 actorId: PINNED_ACTOR_ID,
3233 clientId: PINNED_CLIENT_ID,
3334 };
assistant/packages/server/src/routes.ts+2−1
@@ -10,6 +10,7 @@ import { createAgent } from "@assistant/core"; · 9 unmodified lines
1010 import type { TaskEvent } from "@assistant/core";
1111 import { ConvexHttpClient } from "convex/browser";
1212 import { api } from "@executor/convex/_generated/api";
13+import type { Id } from "@executor/convex/_generated/dataModel";
1314
1415 // ---------------------------------------------------------------------------
1516 // Config
@@ -17,7 +18,7 @@ import { api } from "@executor/convex/_generated/api"; · 1 unmodified line
1718
1819 export interface ServerOptions {
1920 readonly executorUrl: string;
20− readonly workspaceId: string;
21+ readonly workspaceId: Id<"workspaces">;
2122 readonly actorId: string;
2223 readonly clientId?: string;
2324 readonly context?: string;
executor/apps/web/src/app/sign-in/route.ts+1−1
@@ -93,7 +93,7 @@ async function resolveOrganizationHint(request: NextRequest): Promise<string | u · 92 unmodified lines
9393 try {
9494 const workosOrganizationId = await convexClient.query(
9595 api.organizations.resolveWorkosOrganizationId,
96− { organizationId: organizationHint as unknown as Id<"organizations"> },
96+ { organizationId: organizationHint as Id<"organizations"> },
9797 );
9898 return typeof workosOrganizationId === "string" ? workosOrganizationId : undefined;
9999 } catch {
executor/apps/web/src/app/sign-up/route.ts+1−1
@@ -93,7 +93,7 @@ async function resolveOrganizationHint(request: NextRequest): Promise<string | u · 92 unmodified lines
9393 try {
9494 const workosOrganizationId = await convexClient.query(
9595 api.organizations.resolveWorkosOrganizationId,
96− { organizationId: organizationHint as unknown as Id<"organizations"> },
96+ { organizationId: organizationHint as Id<"organizations"> },
9797 );
9898 return typeof workosOrganizationId === "string" ? workosOrganizationId : undefined;
9999 } catch {
executor/apps/web/src/lib/session-context.tsx+2−2
@@ -231,7 +231,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 230 unmodified lines
231231 localStorage.setItem(SESSION_KEY, context.sessionId);
232232 localStorage.setItem(ACTIVE_WORKSPACE_KEY, context.workspaceId);
233233 setStoredSessionId(context.sessionId);
234− setActiveWorkspaceId(context.workspaceId as Id<"workspaces">);
234+ setActiveWorkspaceId(context.workspaceId);
235235 setManualGuestContext(context);
236236 } catch (cause) {
237237 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
401401 if (guestContext) {
402402 return [
403403 {
404− id: guestContext.workspaceId as Id<"workspaces">,
404+ id: guestContext.workspaceId,
405405 docId: null,
406406 name: "Anonymous Workspace",
407407 organizationId: null,
executor/convex/_generated/api.d.ts+15−15
@@ -829,7 +829,7 @@ export declare const internal: { · 828 unmodified lines
829829 },
830830 any
831831 >;
832− deleteAnonymousSessionsMissingAccountId: FunctionReference<
832+ cleanupAccessPolicyEmptyStringSentinels: FunctionReference<
833833 "mutation",
834834 "internal",
835835 {
@@ -841,7 +841,19 @@ export declare const internal: { · 5 unmodified lines
841841 },
842842 any
843843 >;
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<
845857 "mutation",
846858 "internal",
847859 {
@@ -853,7 +865,7 @@ export declare const internal: { · 5 unmodified lines
853865 },
854866 any
855867 >;
856− deleteInvitesMissingProvider: FunctionReference<
868+ deleteAnonymousSessionsMissingUserId: FunctionReference<
857869 "mutation",
858870 "internal",
859871 {
@@ -921,12 +933,6 @@ export declare const internal: { · 61 unmodified lines
921933 { maxAgeMs: number; specUrl: string; version: string },
922934 any
923935 >;
924− pruneExpired: FunctionReference<
925− "mutation",
926− "internal",
927− { batchSize?: number; maxAgeMs: number },
928− any
929− >;
930936 putEntry: FunctionReference<
931937 "mutation",
932938 "internal",
@@ -954,12 +960,6 @@ export declare const internal: { · 21 unmodified lines
954960 >;
955961 };
956962 workspaceToolCache: {
957− getDtsStorageIds: FunctionReference<
958− "query",
959− "internal",
960− { workspaceId: Id<"workspaces"> },
961− any
962− >;
963963 getEntry: FunctionReference<
964964 "query",
965965 "internal",
executor/convex/access-controls.test.ts+0−1
@@ -82,7 +82,6 @@ async function seedUser( · 81 unmodified lines
8282 organizationId,
8383 slug: `ws-${subject}`,
8484 name: opts.workspaceName ?? `${name}'s Workspace`,
85− plan: "free",
8685 createdByAccountId: accountId,
8786 createdAt: now,
8887 updatedAt: now,
executor/convex/auth.ts+0−2
@@ -309,7 +309,6 @@ async function ensurePersonalWorkspace( · 308 unmodified lines
309309 organizationId,
310310 slug: `${baseSlug}-${opts.workosUserId.slice(-6)}`,
311311 name: personalNames.workspaceName,
312− plan: "free",
313312 createdByAccountId: accountId,
314313 createdAt: opts.now,
315314 updatedAt: opts.now,
@@ -480,7 +479,6 @@ const workosEventHandlers = { · 164 unmodified lines
480479 organizationId: organization._id,
481480 slug: "default",
482481 name: "Default Workspace",
483− plan: "free",
484482 createdByAccountId: organization.createdByAccountId,
485483 createdAt: now,
486484 updatedAt: now,
executor/convex/database.test.ts+0−1
@@ -25,7 +25,6 @@ async function seedWorkspace(t: ReturnType<typeof setup>, name = "test-ws"): Pro · 24 unmodified lines
2525 name,
2626 slug: name,
2727 organizationId: orgId,
28− plan: "free",
2928 createdAt: Date.now(),
3029 updatedAt: Date.now(),
3130 });
executor/convex/database.ts+14−30
@@ -27,21 +27,6 @@ const credentialProviderValidator = v.union( · 26 unmodified lines
2727 const toolSourceTypeValidator = v.union(v.literal("mcp"), v.literal("openapi"), v.literal("graphql"));
2828 const agentTaskStatusValidator = v.union(v.literal("running"), v.literal("completed"), v.literal("failed"));
2929
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−
4530 function slugify(input: string): string {
4631 const slug = input
4732 .toLowerCase()
@@ -118,8 +103,8 @@ function mapTask(doc: Doc<"tasks">) { · 70 unmodified lines
118103 timeoutMs: typeof doc.timeoutMs === "number" ? doc.timeoutMs : DEFAULT_TIMEOUT_MS,
119104 metadata: asRecord(doc.metadata),
120105 workspaceId: doc.workspaceId,
121− actorId: optionalFromNormalized(doc.actorId),
122− clientId: optionalFromNormalized(doc.clientId),
106+ actorId: doc.actorId,
107+ clientId: doc.clientId,
123108 createdAt: doc.createdAt,
124109 updatedAt: doc.updatedAt,
125110 startedAt: doc.startedAt,
@@ -149,8 +134,8 @@ function mapPolicy(doc: Doc<"accessPolicies">) { · 23 unmodified lines
149134 return {
150135 id: doc.policyId,
151136 workspaceId: doc.workspaceId,
152− actorId: optionalFromNormalized(doc.actorId),
153− clientId: optionalFromNormalized(doc.clientId),
137+ actorId: doc.actorId,
138+ clientId: doc.clientId,
154139 toolPathPattern: doc.toolPathPattern,
155140 decision: doc.decision,
156141 priority: doc.priority,
@@ -165,7 +150,7 @@ function mapCredential(doc: Doc<"sourceCredentials">) { · 8 unmodified lines
165150 workspaceId: doc.workspaceId,
166151 sourceKey: doc.sourceKey,
167152 scope: doc.scope,
168− actorId: optionalFromNormalized(doc.actorId),
153+ actorId: doc.actorId || undefined,
169154 provider: doc.provider ?? "managed",
170155 secretJson: asRecord(doc.secretJson),
171156 createdAt: doc.createdAt,
@@ -242,7 +227,6 @@ async function ensureAnonymousIdentity( · 70 unmodified lines
242227 organizationId,
243228 slug: `anonymous-${crypto.randomUUID().slice(0, 8)}`,
244229 name: anonymousWorkspaceName,
245− plan: "free",
246230 createdByAccountId: account._id,
247231 createdAt: now,
248232 updatedAt: now,
@@ -351,8 +335,8 @@ export const createTask = internalMutation({ · 102 unmodified lines
351335 code: args.code,
352336 runtimeId: args.runtimeId,
353337 workspaceId: args.workspaceId,
354− actorId: normalizeOptional(args.actorId),
355− clientId: normalizeOptional(args.clientId),
338+ actorId: args.actorId?.trim() || undefined,
339+ clientId: args.clientId?.trim() || undefined,
356340 status: "queued",
357341 timeoutMs: args.timeoutMs ?? DEFAULT_TIMEOUT_MS,
358342 metadata: asRecord(args.metadata),
@@ -720,7 +704,7 @@ export const bootstrapAnonymousSession = internalMutation({ · 361 unmodified lines
720704 args: { sessionId: v.optional(v.string()) },
721705 handler: async (ctx, args) => {
722706 const now = Date.now();
723− const requestedSessionId = normalizeOptional(args.sessionId);
707+ const requestedSessionId = args.sessionId?.trim() || "";
724708 const allowRequestedSessionId = requestedSessionId?.startsWith("mcp_") ?? false;
725709
726710 if (requestedSessionId) {
@@ -814,8 +798,8 @@ export const upsertAccessPolicy = internalMutation({ · 87 unmodified lines
814798 if (existing) {
815799 await ctx.db.patch(existing._id, {
816800 workspaceId: args.workspaceId,
817− actorId: normalizeOptional(args.actorId),
818− clientId: normalizeOptional(args.clientId),
801+ actorId: args.actorId?.trim() || undefined,
802+ clientId: args.clientId?.trim() || undefined,
819803 toolPathPattern: args.toolPathPattern,
820804 decision: args.decision,
821805 priority: args.priority ?? 100,
@@ -825,8 +809,8 @@ export const upsertAccessPolicy = internalMutation({ · 3 unmodified lines
825809 await ctx.db.insert("accessPolicies", {
826810 policyId,
827811 workspaceId: args.workspaceId,
828− actorId: normalizeOptional(args.actorId),
829− clientId: normalizeOptional(args.clientId),
812+ actorId: args.actorId?.trim() || undefined,
813+ clientId: args.clientId?.trim() || undefined,
830814 toolPathPattern: args.toolPathPattern,
831815 decision: args.decision,
832816 priority: args.priority ?? 100,
@@ -877,7 +861,7 @@ export const upsertCredential = internalMutation({ · 44 unmodified lines
877861 },
878862 handler: async (ctx, args) => {
879863 const now = Date.now();
880− const actorId = args.scope === "actor" ? normalizeOptional(args.actorId) : "";
864+ const actorId = args.scope === "actor" ? (args.actorId?.trim() || "") : "";
881865
882866 const existing = await ctx.db
883867 .query("sourceCredentials")
@@ -970,7 +954,7 @@ export const resolveCredential = internalQuery({ · 86 unmodified lines
970954 },
971955 handler: async (ctx, args) => {
972956 if (args.scope === "actor") {
973− const actorId = normalizeOptional(args.actorId);
957+ const actorId = args.actorId?.trim() || "";
974958 if (!actorId) {
975959 return null;
976960 }
executor/convex/executor.ts+1−4
@@ -4,6 +4,7 @@ import type { Id } from "./_generated/dataModel"; · 3 unmodified lines
44 import type { MutationCtx } from "./_generated/server";
55 import { internalMutation } from "./_generated/server";
66 import { workspaceMutation } from "../lib/functionBuilders";
7+import { actorIdForAccount } from "../lib/identity";
78 import type { ApprovalRecord, TaskRecord } from "../lib/types";
89
910 const DEFAULT_TIMEOUT_MS = 300_000;
@@ -20,10 +21,6 @@ async function publishTaskEvent( · 10 unmodified lines
2021 await ctx.runMutation(internal.database.createTaskEvent, input);
2122 }
2223
23−function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }) {
24− return account.provider === "anonymous" ? account.providerAccountId : account._id;
25−}
26−
2724 async function createTaskRecord(
2825 ctx: MutationCtx,
2926 args: {
executor/convex/executorNode.ts+63−35
@@ -2,10 +2,13 @@ · 1 unmodified line
22
33 import { v } from "convex/values";
44 import { internal } from "./_generated/api";
5+import type { Id } from "./_generated/dataModel";
56 import { action, internalAction } from "./_generated/server";
7+import type { ActionCtx } from "./_generated/server";
68 import { InProcessExecutionAdapter } from "../lib/adapters/in_process_execution_adapter";
79 import { resolveCredentialPayload } from "../lib/credential_providers";
810 import { APPROVAL_DENIED_PREFIX } from "../lib/execution_constants";
11+import { actorIdForAccount } from "../lib/identity";
912 import { runCodeWithAdapter } from "../lib/runtimes/runtime_core";
1013 import { createDiscoverTool } from "../lib/tool_discovery";
1114 import {
@@ -17,6 +20,9 @@ import { · 5 unmodified lines
1720 serializeTools,
1821 type PreparedOpenApiSpec,
1922 type ExternalToolSourceConfig,
23+ type McpToolSourceConfig,
24+ type OpenApiToolSourceConfig,
25+ type GraphqlToolSourceConfig,
2026 type WorkspaceToolSnapshot,
2127 } from "../lib/tool_sources";
2228 import { DEFAULT_TOOLS } from "../lib/tools";
@@ -173,27 +179,23 @@ function normalizeExternalToolSource(raw: { · 150 unmodified lines
173179 name: string;
174180 config: Record<string, unknown>;
175181 }): ExternalToolSourceConfig {
176− const merged = {
177− type: raw.type,
178− name: raw.name,
179− ...raw.config,
180− } as Record<string, unknown>;
182+ const config = raw.config;
181183
182184 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) {
184186 throw new Error(`MCP source '${raw.name}' missing url`);
185187 }
186188
187189 if (
188− merged.transport !== undefined
189− && merged.transport !== "sse"
190− && merged.transport !== "streamable-http"
190+ config.transport !== undefined
191+ && config.transport !== "sse"
192+ && config.transport !== "streamable-http"
191193 ) {
192194 throw new Error(`MCP source '${raw.name}' has invalid transport`);
193195 }
194196
195− if (merged.queryParams !== undefined) {
196− const queryParams = merged.queryParams;
197+ if (config.queryParams !== undefined) {
198+ const queryParams = config.queryParams;
197199 if (!queryParams || typeof queryParams !== "object" || Array.isArray(queryParams)) {
198200 throw new Error(`MCP source '${raw.name}' queryParams must be an object`);
199201 }
@@ -205,22 +207,52 @@ function normalizeExternalToolSource(raw: { · 5 unmodified lines
205207 }
206208 }
207209
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;
209220 }
210221
211222 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) {
213224 throw new Error(`GraphQL source '${raw.name}' missing endpoint`);
214225 }
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;
216238 }
217239
218− const spec = merged.spec;
219− if (typeof spec !== "string" && typeof spec !== "object") {
240+ const spec = config.spec;
241+ if (typeof spec !== "string" && (typeof spec !== "object" || spec === null)) {
220242 throw new Error(`OpenAPI source '${raw.name}' missing spec`);
221243 }
222244
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;
224256 }
225257
226258 async function sleep(ms: number): Promise<void> {
@@ -230,7 +262,7 @@ async function sleep(ms: number): Promise<void> { · 3 unmodified lines
230262 const baseTools = new Map<string, ToolDefinition>(DEFAULT_TOOLS.map((tool) => [tool.path, tool]));
231263 interface DtsStorageEntry {
232264 sourceKey: string;
233− storageId: string;
265+ storageId: Id<"_storage">;
234266 }
235267
236268 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
239271 const OPENAPI_CACHE_VERSION = "v14";
240272
241273 async function publish(
242− ctx: any,
274+ ctx: ActionCtx,
243275 taskId: string,
244276 eventName: "task" | "approval",
245277 type: string,
@@ -253,7 +285,7 @@ async function publish( · 7 unmodified lines
253285 });
254286 }
255287
256−async function waitForApproval(ctx: any, approvalId: string): Promise<"approved" | "denied"> {
288+async function waitForApproval(ctx: ActionCtx, approvalId: string): Promise<"approved" | "denied"> {
257289 while (true) {
258290 const approval = await ctx.runQuery(internal.database.getApproval, { approvalId });
259291 if (!approval) {
@@ -278,7 +310,7 @@ async function waitForApproval(ctx: any, approvalId: string): Promise<"approved" · 18 unmodified lines
278310 * No size limits — Convex file storage handles multi-MB specs without issue.
279311 */
280312 async function loadCachedOpenApiSpec(
281− ctx: any,
313+ ctx: ActionCtx,
282314 specUrl: string,
283315 sourceName: string,
284316 ): Promise<PreparedOpenApiSpec> {
@@ -327,7 +359,7 @@ async function loadCachedOpenApiSpec( · 42 unmodified lines
327359 }
328360
329361 async function loadSourceTools(
330− ctx: any,
362+ ctx: ActionCtx,
331363 source: ExternalToolSourceConfig,
332364 ): Promise<{ tools: ToolDefinition[]; warnings: string[] }> {
333365 if (source.type === "openapi" && typeof source.spec === "string") {
@@ -356,7 +388,7 @@ interface WorkspaceToolsResult { · 22 unmodified lines
356388 dtsStorageIds: DtsStorageEntry[];
357389 }
358390
359−async function getWorkspaceTools(ctx: any, workspaceId: string): Promise<WorkspaceToolsResult> {
391+async function getWorkspaceTools(ctx: ActionCtx, workspaceId: Id<"workspaces">): Promise<WorkspaceToolsResult> {
360392 const sources = (await ctx.runQuery(internal.database.listToolSources, { workspaceId }))
361393 .filter((source: { enabled: boolean }) => source.enabled);
362394 const signature = sourceSignature(workspaceId, sources);
@@ -440,7 +472,7 @@ async function getWorkspaceTools(ctx: any, workspaceId: string): Promise<Workspa · 77 unmodified lines
440472 dtsEntries.map(async (entry) => {
441473 const dtsBlob = new Blob([entry.content], { type: "text/plain" });
442474 const sid = await ctx.storage.store(dtsBlob);
443− return { sourceKey: entry.sourceKey, storageId: String(sid) };
475+ return { sourceKey: entry.sourceKey, storageId: sid };
444476 }),
445477 );
446478 dtsStorageIds = storedDts;
@@ -463,7 +495,7 @@ async function getWorkspaceTools(ctx: any, workspaceId: string): Promise<Workspa · 16 unmodified lines
463495 workspaceId,
464496 signature,
465497 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 })),
467499 toolCount: allTools.length,
468500 sizeBytes: json.length,
469501 });
@@ -608,8 +640,8 @@ function listVisibleToolDescriptors( · 138 unmodified lines
608640 }
609641
610642 async function listToolsForContext(
611− ctx: any,
612− context: { workspaceId: string; actorId?: string; clientId?: string },
643+ ctx: ActionCtx,
644+ context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string },
613645 ): Promise<ToolDescriptor[]> {
614646 const [result, policies] = await Promise.all([
615647 getWorkspaceTools(ctx, context.workspaceId),
@@ -621,8 +653,8 @@ async function listToolsForContext( · 5 unmodified lines
621653 }
622654
623655 async function listToolsWithWarningsForContext(
624− ctx: any,
625− context: { workspaceId: string; actorId?: string; clientId?: string },
656+ ctx: ActionCtx,
657+ context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string },
626658 ): Promise<{
627659 tools: ToolDescriptor[];
628660 warnings: string[];
@@ -656,10 +688,6 @@ async function listToolsWithWarningsForContext( · 27 unmodified lines
656688 };
657689 }
658690
659−function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }): string {
660− return account.provider === "anonymous" ? account.providerAccountId : account._id;
661−}
662−
663691 function isToolAllowedForTask(
664692 task: TaskRecord,
665693 toolPath: string,
@@ -672,7 +700,7 @@ function isToolAllowedForTask( · 6 unmodified lines
672700 }
673701
674702 async function resolveCredentialHeaders(
675− ctx: any,
703+ ctx: ActionCtx,
676704 spec: ToolCredentialSpec,
677705 task: TaskRecord,
678706 ): Promise<ResolvedToolCredential | null> {
@@ -774,7 +802,7 @@ function getGraphqlDecision( · 95 unmodified lines
774802 return { decision: worstDecision, effectivePaths: fieldPaths };
775803 }
776804
777−async function invokeTool(ctx: any, task: TaskRecord, call: ToolCallRequest): Promise<unknown> {
805+async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCallRequest): Promise<unknown> {
778806 const { toolPath, input, callId } = call;
779807 const policies = await ctx.runQuery(internal.database.listAccessPolicies, { workspaceId: task.workspaceId });
780808 const typedPolicies = policies as AccessPolicyRecord[];
executor/convex/http.ts+25−17
@@ -110,13 +110,23 @@ async function verifyMcpToken( · 109 unmodified lines
110110 }
111111 }
112112
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+
113122 function parseMcpContext(url: URL): {
114− workspaceId: string;
123+ workspaceId: Id<"workspaces">;
115124 clientId?: string;
116125 sessionId?: string;
117126 } | undefined {
118− const workspaceId = url.searchParams.get("workspaceId");
119− if (!workspaceId) return undefined;
127+ const raw = url.searchParams.get("workspaceId");
128+ if (!raw) return undefined;
129+ const workspaceId = parseWorkspaceId(raw);
120130 const clientId = url.searchParams.get("clientId") ?? undefined;
121131 const sessionId = url.searchParams.get("sessionId") ?? undefined;
122132 return { workspaceId, clientId, sessionId };
@@ -162,7 +172,7 @@ const mcpHandler = httpAction(async (ctx, request) => { · 39 unmodified lines
162172 try {
163173 if (mcpAuthConfig.enabled && auth?.subject) {
164174 const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForWorkosSubject, {
165− workspaceId: requestedContext.workspaceId as Id<"workspaces">,
175+ workspaceId: requestedContext.workspaceId,
166176 subject: auth.subject,
167177 });
168178
@@ -177,7 +187,7 @@ const mcpHandler = httpAction(async (ctx, request) => { · 8 unmodified lines
177187 }
178188
179189 const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForRequest, {
180− workspaceId: requestedContext.workspaceId as Id<"workspaces">,
190+ workspaceId: requestedContext.workspaceId,
181191 sessionId: requestedContext.sessionId,
182192 });
183193
@@ -209,18 +219,17 @@ const mcpHandler = httpAction(async (ctx, request) => { · 25 unmodified lines
209219 timeoutMs?: number;
210220 runtimeId?: string;
211221 metadata?: Record<string, unknown>;
212− workspaceId: string;
222+ workspaceId: Id<"workspaces">;
213223 actorId: string;
214224 clientId?: string;
215225 }) => {
216226 return (await ctx.runMutation(internal.executor.createTaskInternal, {
217227 ...input,
218− workspaceId: input.workspaceId as Id<"workspaces">,
219228 })) as { task: TaskRecord };
220229 },
221− getTask: async (taskId: string, workspaceId?: string) => {
230+ getTask: async (taskId: string, workspaceId?: Id<"workspaces">) => {
222231 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;
224233 }
225234 return null;
226235 },
@@ -230,15 +239,15 @@ const mcpHandler = httpAction(async (ctx, request) => { · 3 unmodified lines
230239 bootstrapAnonymousContext: async (sessionId?: string) => {
231240 return (await ctx.runMutation(internal.database.bootstrapAnonymousSession, { sessionId })) as AnonymousContext;
232241 },
233− listTools: async (toolContext?: { workspaceId: string; actorId?: string; clientId?: string }) => {
242+ listTools: async (toolContext?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => {
234243 if (!toolContext) {
235244 return [];
236245 }
237246
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[];
239248 },
240− listToolsForTypecheck: async (toolContext: { workspaceId: string; actorId?: string; clientId?: string }) => {
241− const result = await ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, { ...toolContext, workspaceId: toolContext.workspaceId as Id<"workspaces"> }) as {
249+ listToolsForTypecheck: async (toolContext: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => {
250+ const result = await ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, { ...toolContext }) as {
242251 tools: ToolDescriptor[];
243252 dtsUrls?: Record<string, string>;
244253 };
@@ -248,11 +257,11 @@ const mcpHandler = httpAction(async (ctx, request) => { · 3 unmodified lines
248257 dtsUrls: result.dtsUrls ?? {},
249258 };
250259 },
251− listPendingApprovals: async (workspaceId: string) => {
252− return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId: workspaceId as Id<"workspaces"> })) as PendingApprovalRecord[];
260+ listPendingApprovals: async (workspaceId: Id<"workspaces">) => {
261+ return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId })) as PendingApprovalRecord[];
253262 },
254263 resolveApproval: async (input: {
255− workspaceId: string;
264+ workspaceId: Id<"workspaces">;
256265 approvalId: string;
257266 decision: "approved" | "denied";
258267 reviewerId?: string;
@@ -260,7 +269,6 @@ const mcpHandler = httpAction(async (ctx, request) => { · 1 unmodified line
260269 }) => {
261270 return await ctx.runMutation(internal.executor.resolveApprovalInternal, {
262271 ...input,
263− workspaceId: input.workspaceId as Id<"workspaces">,
264272 });
265273 },
266274 };
executor/convex/invites.ts+0−4
@@ -185,15 +185,12 @@ export const create = organizationMutation({ · 184 unmodified lines
185185 }
186186 const inviterWorkosUserId = ctx.account.providerAccountId;
187187
188− const provider = "workos";
189−
190188 const inviteId = await ctx.db.insert("invites", {
191189 organizationId: ctx.organizationId,
192190 workspaceId: args.workspaceId,
193191 email: normalizedEmail,
194192 role: args.role,
195193 status: "pending",
196− provider,
197194 invitedByAccountId: ctx.account._id,
198195 expiresAt,
199196 createdAt: now,
@@ -222,7 +219,6 @@ export const create = organizationMutation({ · 22 unmodified lines
222219 expiresAt: invite.expiresAt,
223220 },
224221 delivery: {
225− provider,
226222 providerInviteId: invite.providerInviteId ?? null,
227223 state: "queued",
228224 },
executor/convex/migrations.ts+20−11
@@ -81,17 +81,6 @@ export const deleteSourceCredentialsMissingProvider = migrations.define({ · 80 unmodified lines
8181 },
8282 });
8383
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−
9584 export const deleteAnonymousSessionsMissingUserId = migrations.define({
9685 table: "anonymousSessions",
9786 migrateOne: async (ctx, session) => {
@@ -112,4 +101,24 @@ export const backfillDtsStorageIds = migrations.define({ · 14 unmodified lines
112101 },
113102 });
114103
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+
115124 export const run = migrations.runner();
executor/convex/openApiSpecCache.ts+0−32
@@ -66,35 +66,3 @@ export const putEntry = internalMutation({ · 65 unmodified lines
6666 });
6767 },
6868 });
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−});
executor/convex/organizations.ts+0−1
@@ -75,7 +75,6 @@ export const create = authedMutation({ · 74 unmodified lines
7575 organizationId,
7676 slug: "default",
7777 name: "Default Workspace",
78− plan: "free",
7978 createdByAccountId: account._id,
8079 createdAt: now,
8180 updatedAt: now,
executor/convex/schema.ts+7−20
@@ -25,7 +25,6 @@ const inviteStatus = v.union( · 24 unmodified lines
2525 v.literal("revoked"),
2626 v.literal("failed"),
2727 );
28−const inviteProvider = v.literal("workos");
2928 const taskStatus = v.union(
3029 v.literal("queued"),
3130 v.literal("running"),
@@ -58,8 +57,7 @@ export default defineSchema({ · 26 unmodified lines
5857 updatedAt: v.number(),
5958 lastLoginAt: v.optional(v.number()),
6059 })
61− .index("by_provider", ["provider", "providerAccountId"])
62− .index("by_email", ["email"]),
60+ .index("by_provider", ["provider", "providerAccountId"]),
6361
6462 workspaces: defineTable({
6563 workosOrgId: v.optional(v.string()),
@@ -67,7 +65,6 @@ export default defineSchema({ · 1 unmodified line
6765 slug: v.string(),
6866 name: v.string(),
6967 iconStorageId: v.optional(v.id("_storage")),
70− plan: v.string(),
7168 createdByAccountId: v.optional(v.id("accounts")),
7269 createdAt: v.number(),
7370 updatedAt: v.number(),
@@ -75,7 +72,6 @@ export default defineSchema({ · 1 unmodified line
7572 .index("by_workos_org_id", ["workosOrgId"])
7673 .index("by_organization_created", ["organizationId", "createdAt"])
7774 .index("by_organization_slug", ["organizationId", "slug"])
78− .index("by_creator_created", ["createdByAccountId", "createdAt"])
7975 .index("by_slug", ["slug"]),
8076
8177 organizations: defineTable({
@@ -106,7 +102,6 @@ export default defineSchema({ · 24 unmodified lines
106102 .index("by_org", ["organizationId"])
107103 .index("by_org_account", ["organizationId", "accountId"])
108104 .index("by_account", ["accountId"])
109− .index("by_org_status", ["organizationId", "status"])
110105 .index("by_org_billable_status", ["organizationId", "billable", "status"]),
111106
112107 workspaceMembers: defineTable({
@@ -121,7 +116,6 @@ export default defineSchema({ · 8 unmodified lines
121116 .index("by_workspace", ["workspaceId"])
122117 .index("by_workspace_account", ["workspaceId", "accountId"])
123118 .index("by_account", ["accountId"])
124− .index("by_workspace_status", ["workspaceId", "status"])
125119 .index("by_workos_membership_id", ["workosOrgMembershipId"]),
126120
127121 invites: defineTable({
@@ -130,7 +124,6 @@ export default defineSchema({ · 2 unmodified lines
130124 email: v.string(),
131125 role: orgRole,
132126 status: inviteStatus,
133− provider: inviteProvider,
134127 providerInviteId: v.optional(v.string()),
135128 invitedByAccountId: v.id("accounts"),
136129 expiresAt: v.number(),
@@ -139,7 +132,6 @@ export default defineSchema({ · 2 unmodified lines
139132 updatedAt: v.number(),
140133 })
141134 .index("by_org", ["organizationId"])
142− .index("by_org_status_created", ["organizationId", "status", "createdAt"])
143135 .index("by_org_email_status", ["organizationId", "email", "status"]),
144136
145137 billingCustomers: defineTable({
@@ -182,8 +174,8 @@ export default defineSchema({ · 36 unmodified lines
182174 code: v.string(),
183175 runtimeId: v.string(),
184176 workspaceId: v.id("workspaces"),
185− actorId: v.string(),
186− clientId: v.string(),
177+ actorId: v.optional(v.string()),
178+ clientId: v.optional(v.string()),
187179 status: taskStatus,
188180 timeoutMs: v.number(),
189181 metadata: v.any(),
@@ -213,7 +205,6 @@ export default defineSchema({ · 23 unmodified lines
213205 resolvedAt: v.optional(v.number()),
214206 })
215207 .index("by_approval_id", ["approvalId"])
216− .index("by_task", ["taskId"])
217208 .index("by_workspace_created", ["workspaceId", "createdAt"])
218209 .index("by_workspace_status_created", ["workspaceId", "status", "createdAt"]),
219210
@@ -225,14 +216,13 @@ export default defineSchema({ · 5 unmodified lines
225216 payload: v.any(),
226217 createdAt: v.number(),
227218 })
228− .index("by_sequence", ["sequence"])
229219 .index("by_task_sequence", ["taskId", "sequence"]),
230220
231221 accessPolicies: defineTable({
232222 policyId: v.string(),
233223 workspaceId: v.id("workspaces"),
234− actorId: v.string(),
235− clientId: v.string(),
224+ actorId: v.optional(v.string()),
225+ clientId: v.optional(v.string()),
236226 toolPathPattern: v.string(),
237227 decision: policyDecision,
238228 priority: v.number(),
@@ -253,7 +243,6 @@ export default defineSchema({ · 14 unmodified lines
253243 createdAt: v.number(),
254244 updatedAt: v.number(),
255245 })
256− .index("by_credential_id", ["credentialId"])
257246 .index("by_workspace_created", ["workspaceId", "createdAt"])
258247 .index("by_workspace_source_scope_actor", ["workspaceId", "sourceKey", "scope", "actorId"]),
259248
@@ -269,8 +258,7 @@ export default defineSchema({ · 9 unmodified lines
269258 })
270259 .index("by_source_id", ["sourceId"])
271260 .index("by_workspace_updated", ["workspaceId", "updatedAt"])
272− .index("by_workspace_name", ["workspaceId", "name"])
273− .index("by_workspace_enabled_updated", ["workspaceId", "enabled", "updatedAt"]),
261+ .index("by_workspace_name", ["workspaceId", "name"]),
274262
275263 agentTasks: defineTable({
276264 agentTaskId: v.string(),
@@ -286,8 +274,7 @@ export default defineSchema({ · 9 unmodified lines
286274 updatedAt: v.number(),
287275 })
288276 .index("by_agent_task_id", ["agentTaskId"])
289− .index("by_workspace_created", ["workspaceId", "createdAt"])
290− .index("by_requester_created", ["requesterId", "createdAt"]),
277+ .index("by_workspace_created", ["workspaceId", "createdAt"]),
291278
292279 openApiSpecCache: defineTable({
293280 specUrl: v.string(),
executor/convex/workspace.ts+1−4
@@ -2,6 +2,7 @@ import { v } from "convex/values"; · 1 unmodified line
22 import { internal } from "./_generated/api";
33 import { mutation, query } from "./_generated/server";
44 import { workspaceMutation, workspaceQuery } from "../lib/functionBuilders";
5+import { actorIdForAccount } from "../lib/identity";
56
67 const approvalStatusValidator = v.union(v.literal("pending"), v.literal("approved"), v.literal("denied"));
78 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
910 const credentialProviderValidator = v.union(v.literal("managed"), v.literal("workos-vault"));
1011 const toolSourceTypeValidator = v.union(v.literal("mcp"), v.literal("openapi"), v.literal("graphql"));
1112
12−function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }): string {
13− return account.provider === "anonymous" ? account.providerAccountId : account._id;
14−}
15−
1613 function redactCredential<T extends { secretJson: Record<string, unknown> }>(credential: T): T {
1714 return {
1815 ...credential,
executor/convex/workspaceAuthInternal.ts+1−4
@@ -1,15 +1,12 @@
11 import { v } from "convex/values";
22 import { internalQuery } from "./_generated/server";
33 import {
4+ actorIdForAccount,
45 requireWorkspaceAccessForAccount,
56 resolveAccountForRequest,
67 resolveWorkosAccountBySubject,
78 } from "../lib/identity";
89
9−function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }): string {
10− return account.provider === "anonymous" ? account.providerAccountId : account._id;
11−}
12−
1310 export const getWorkspaceAccessForRequest = internalQuery({
1411 args: {
1512 workspaceId: v.id("workspaces"),
executor/convex/workspaceToolCache.ts+0−18
@@ -72,21 +72,3 @@ export const putEntry = internalMutation({ · 71 unmodified lines
7272 });
7373 },
7474 });
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−});
executor/convex/workspaces.ts+0−1
@@ -193,7 +193,6 @@ export const create = authedMutation({ · 192 unmodified lines
193193 slug,
194194 name,
195195 iconStorageId: args.iconStorageId,
196− plan: "free",
197196 createdByAccountId: account._id,
198197 createdAt: now,
199198 updatedAt: now,
executor/lib/identity.ts+4−0
@@ -158,3 +158,7 @@ export function isAdminRole(role: string): boolean { · 157 unmodified lines
158158 export function canManageBilling(role: string): boolean {
159159 return role === "owner" || role === "billing_admin";
160160 }
161+
162+export function actorIdForAccount(account: { _id: string; provider: string; providerAccountId: string }): string {
163+ return account.provider === "anonymous" ? account.providerAccountId : account._id;
164+}
executor/lib/mcp-server.test.ts+2−1
@@ -3,6 +3,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; · 2 unmodified lines
33 import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
44 import { handleMcpRequest } from "./mcp_server";
55 import type { LiveTaskEvent } from "./events";
6+import type { Id } from "../convex/_generated/dataModel";
67 import type { AnonymousContext, CreateTaskInput, TaskRecord, ToolDescriptor } from "./types";
78
89 class FakeMcpService {
@@ -75,7 +76,7 @@ class FakeMcpService { · 66 unmodified lines
7576 const now = Date.now();
7677 const context: AnonymousContext = {
7778 sessionId: sessionId ?? `anon_session_${crypto.randomUUID()}`,
78− workspaceId: `ws_${crypto.randomUUID()}`,
79+ workspaceId: `ws_${crypto.randomUUID()}` as Id<"workspaces">,
7980 actorId: `anon_${crypto.randomUUID()}`,
8081 clientId: "mcp",
8182 accountId: `account_${crypto.randomUUID()}`,
executor/lib/mcp_server.ts+10−9
@@ -10,6 +10,7 @@ import type { · 9 unmodified lines
1010 TaskRecord,
1111 ToolDescriptor,
1212 } from "./types";
13+import type { Id } from "../convex/_generated/dataModel";
1314
1415 function getTaskTerminalState(status: string): boolean {
1516 return status === "completed" || status === "failed" || status === "timed_out" || status === "denied";
@@ -21,16 +22,16 @@ function getTaskTerminalState(status: string): boolean { · 5 unmodified lines
2122
2223 interface McpExecutorService {
2324 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>;
2526 subscribe(taskId: string, listener: (event: LiveTaskEvent) => void): () => void;
2627 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[]>;
2829 listToolsForTypecheck?(
29− context: { workspaceId: string; actorId?: string; clientId?: string },
30+ context: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string },
3031 ): Promise<{ tools: ToolDescriptor[]; dtsUrls: Record<string, string> }>;
31− listPendingApprovals?(workspaceId: string): Promise<PendingApprovalRecord[]>;
32+ listPendingApprovals?(workspaceId: Id<"workspaces">): Promise<PendingApprovalRecord[]>;
3233 resolveApproval?(input: {
33− workspaceId: string;
34+ workspaceId: Id<"workspaces">;
3435 approvalId: string;
3536 decision: "approved" | "denied";
3637 reviewerId?: string;
@@ -44,7 +45,7 @@ interface ApprovalPromptDecision { · 7 unmodified lines
4445 }
4546
4647 interface ApprovalPromptContext {
47− workspaceId: string;
48+ workspaceId: Id<"workspaces">;
4849 actorId: string;
4950 }
5051
@@ -58,7 +59,7 @@ type ApprovalPrompt = ( · 7 unmodified lines
5859 // ---------------------------------------------------------------------------
5960
6061 export interface McpWorkspaceContext {
61− workspaceId: string;
62+ workspaceId: Id<"workspaces">;
6263 actorId: string;
6364 clientId?: string;
6465 sessionId?: string;
@@ -173,7 +174,7 @@ function summarizeTask(task: TaskRecord): string { · 108 unmodified lines
173174 function waitForTerminalTask(
174175 service: McpExecutorService,
175176 taskId: string,
176− workspaceId: string,
177+ workspaceId: Id<"workspaces">,
177178 waitTimeoutMs: number,
178179 onApprovalPrompt?: ApprovalPrompt,
179180 approvalContext?: ApprovalPromptContext,
@@ -422,7 +423,7 @@ function createRunCodeTool( · 242 unmodified lines
422423 const requestedTimeoutMs = input.timeoutMs ?? 300_000;
423424
424425 // 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 };
426427
427428 if (boundContext) {
428429 context = { ...boundContext, sessionId: input.sessionId ?? boundContext.sessionId };
executor/lib/runtimes/runtime-core.test.ts+2−1
@@ -1,6 +1,7 @@
11 import { expect, test } from "bun:test";
22 import { runCodeWithAdapter } from "./runtime_core";
33 import { loadExternalTools } from "../tool_sources";
4+import type { Id } from "../../convex/_generated/dataModel";
45 import type {
56 ExecutionAdapter,
67 RuntimeOutputEvent,
@@ -34,7 +35,7 @@ function createRuntimeAdapter( · 27 unmodified lines
3435 try {
3536 const context: ToolRunContext = {
3637 taskId: call.runId,
37− workspaceId: "ws_test",
38+ workspaceId: "ws_test" as Id<"workspaces">,
3839 actorId: "actor_test",
3940 clientId: "web",
4041 isToolAllowed: () => true,
executor/lib/tool-discovery.test.ts+10−7
@@ -1,7 +1,10 @@
11 import { expect, test } from "bun:test";
2+import type { Id } from "../convex/_generated/dataModel";
23 import { createDiscoverTool } from "./tool_discovery";
34 import type { ToolDefinition } from "./types";
45
6+const TEST_WORKSPACE_ID = "w" as Id<"workspaces">;
7+
58 test("discover returns aliases and example calls", async () => {
69 const tool = createDiscoverTool([
710 {
@@ -19,7 +22,7 @@ test("discover returns aliases and example calls", async () => { · 11 unmodified lines
1922
2023 const result = await tool.run(
2124 { query: "addnumbers", depth: 2 },
22− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
25+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
2326 ) as {
2427 bestPath: string | null;
2528 results: Array<{
@@ -57,7 +60,7 @@ test("discover example call handles input-shaped args", async () => { · 31 unmodified lines
5760
5861 const result = await tool.run(
5962 { query: "issuecreate", depth: 2 },
60− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
63+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
6164 ) as {
6265 bestPath: string | null;
6366 results: Array<{ exampleCall: string }>;
@@ -86,7 +89,7 @@ test("discover uses compact signatures by default and allows full mode", async ( · 22 unmodified lines
8689
8790 const compactResult = await tool.run(
8891 { query: "linear teams", depth: 2 },
89− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
92+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
9093 ) as {
9194 bestPath: string | null;
9295 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
9497
9598 const fullResult = await tool.run(
9699 { query: "linear teams", depth: 2, compact: false },
97− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
100+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
98101 ) as {
99102 bestPath: string | null;
100103 results: Array<{ description: string; signature: string }>;
@@ -128,7 +131,7 @@ test("discover returns null bestPath when there are no matches", async () => { · 27 unmodified lines
128131
129132 const result = await tool.run(
130133 { query: "totally_unrelated_keyword" },
131− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
134+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
132135 ) as { bestPath: string | null; results: Array<unknown>; total: number };
133136
134137 expect(result.bestPath).toBeNull();
@@ -164,7 +167,7 @@ test("discover bestPath prefers simpler exact intent operation", async () => { · 29 unmodified lines
164167
165168 const result = await tool.run(
166169 { query: "linear issue create", depth: 2 },
167− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
170+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
168171 ) as { bestPath: string | null; results: Array<{ path: string }> };
169172
170173 expect(result.bestPath).toBe("linear.mutation.issuecreate");
@@ -199,7 +202,7 @@ test("discover namespace hint suppresses cross-namespace bestPath", async () => · 28 unmodified lines
199202
200203 const result = await tool.run(
201204 { query: "linear teams list", depth: 2 },
202− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
205+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
203206 ) as { bestPath: string | null; results: Array<{ path: string }> };
204207
205208 expect(result.bestPath).toBe("linear.query.teams");
executor/lib/tool-sources.test.ts+4−1
@@ -1,7 +1,10 @@
11 import { expect, test } from "bun:test";
2+import type { Id } from "../convex/_generated/dataModel";
23 import { loadExternalTools } from "./tool_sources";
34 import type { ExternalToolSourceConfig } from "./tool_sources";
45
6+const TEST_WORKSPACE_ID = "w" as Id<"workspaces">;
7+
58 function makeInlineSpec(tag: string, operationId: string): Record<string, unknown> {
69 return {
710 openapi: "3.0.3",
@@ -461,7 +464,7 @@ test("graphql helper tools generate valid selection sets and envelope responses" · 453 unmodified lines
461464 expect(batchTool).toBeDefined();
462465 expect(rawTool).toBeDefined();
463466
464− const context = { taskId: "t", workspaceId: "w", isToolAllowed: () => true };
467+ const context = { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true };
465468
466469 const teamsResult = await teamsTool!.run({}, context);
467470 const batchResult = await batchTool!.run({ input: { issues: ["one"] } }, context);
executor/lib/types.ts+9−7
@@ -1,5 +1,7 @@
11 // ── Shared types (inlined from @executor/contracts) ──────────────────────────
22
3+import type { Id } from "../convex/_generated/dataModel";
4+
35 export type TaskStatus = "queued" | "running" | "completed" | "failed" | "timed_out" | "denied";
46 export type ApprovalStatus = "pending" | "approved" | "denied";
57 export type PolicyDecision = "allow" | "require_approval" | "deny";
@@ -15,7 +17,7 @@ export interface TaskRecord { · 9 unmodified lines
1517 status: TaskStatus;
1618 timeoutMs: number;
1719 metadata: Record<string, unknown>;
18− workspaceId: string;
20+ workspaceId: Id<"workspaces">;
1921 actorId?: string;
2022 clientId?: string;
2123 createdAt: number;
@@ -55,7 +57,7 @@ export interface TaskEventRecord { · 33 unmodified lines
5557
5658 export interface AccessPolicyRecord {
5759 id: string;
58− workspaceId: string;
60+ workspaceId: Id<"workspaces">;
5961 actorId?: string;
6062 clientId?: string;
6163 toolPathPattern: string;
@@ -67,7 +69,7 @@ export interface AccessPolicyRecord { · 5 unmodified lines
6769
6870 export interface CredentialRecord {
6971 id: string;
70− workspaceId: string;
72+ workspaceId: Id<"workspaces">;
7173 sourceKey: string;
7274 scope: CredentialScope;
7375 actorId?: string;
@@ -79,7 +81,7 @@ export interface CredentialRecord { · 5 unmodified lines
7981
8082 export interface ToolSourceRecord {
8183 id: string;
82− workspaceId: string;
84+ workspaceId: Id<"workspaces">;
8385 name: string;
8486 type: ToolSourceType;
8587 config: Record<string, unknown>;
@@ -112,7 +114,7 @@ export interface OpenApiSourceQuality { · 26 unmodified lines
112114
113115 export interface AnonymousContext {
114116 sessionId: string;
115− workspaceId: string;
117+ workspaceId: Id<"workspaces">;
116118 actorId: string;
117119 clientId: string;
118120 accountId: string;
@@ -128,7 +130,7 @@ export interface CreateTaskInput { · 9 unmodified lines
128130 timeoutMs?: number;
129131 runtimeId?: string;
130132 metadata?: Record<string, unknown>;
131− workspaceId: string;
133+ workspaceId: Id<"workspaces">;
132134 actorId: string;
133135 clientId?: string;
134136 }
@@ -201,7 +203,7 @@ export interface ResolvedToolCredential { · 66 unmodified lines
201203
202204 export interface ToolRunContext {
203205 taskId: string;
204− workspaceId: string;
206+ workspaceId: Id<"workspaces">;
205207 actorId?: string;
206208 clientId?: string;
207209 credential?: ResolvedToolCredential;
executor/lib/workspace-tool-cache.test.ts+6−5
@@ -16,6 +16,8 @@ import { · 15 unmodified lines
1616 } from "./tool_sources";
1717 import type { ToolDefinition } from "./types";
1818
19+const TEST_WORKSPACE_ID = "w" as Id<"workspaces">;
20+
1921 function makeBaseTools(): Map<string, ToolDefinition> {
2022 return new Map([
2123 [
@@ -236,7 +238,7 @@ describe("serializeTools + rehydrateTools round-trip", () => { · 214 unmodified lines
236238 // The rehydrated run should work
237239 const result = await rehydrated[0]!.run(
238240 { message: "hello" },
239− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
241+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
240242 );
241243 expect(result).toBe("hello");
242244 });
@@ -344,11 +346,11 @@ describe("serializeTools + rehydrateTools round-trip", () => { · 101 unmodified lines
344346 try {
345347 const rawResult = await rawTool!.run(
346348 { query: "query viewer { viewer { id } }" },
347− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
349+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
348350 );
349351 const teamsResult = await teamsTool!.run(
350352 {},
351− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
353+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
352354 );
353355
354356 expect(rawResult).toEqual({ data: { viewer: { id: "user_1" } }, errors: [] });
@@ -395,7 +397,7 @@ describe("serializeTools + rehydrateTools round-trip", () => { · 40 unmodified lines
395397 const echoTool = rehydrated.find((t) => t.path === "echo")!;
396398 const result = await echoTool.run(
397399 { message: "round-trip" },
398− { taskId: "t", workspaceId: "w", isToolAllowed: () => true },
400+ { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
399401 );
400402 expect(result).toBe("round-trip");
401403
@@ -487,7 +489,6 @@ describe("workspace tool cache table", () => { · 85 unmodified lines
487489 name: "test-ws",
488490 slug: "test-ws",
489491 organizationId: orgId,
490− plan: "free",
491492 createdAt: Date.now(),
492493 updatedAt: Date.now(),
493494 });
executor/mcp-gateway.ts+24−14
@@ -94,13 +94,23 @@ async function verifyOptionalMcpToken(request: Request): Promise<string | null> · 93 unmodified lines
9494 return payload.sub;
9595 }
9696
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+
97106 function parseRequestedContext(url: URL): {
98− workspaceId?: string;
107+ workspaceId?: Id<"workspaces">;
99108 actorId?: string;
100109 clientId?: string;
101110 sessionId?: string;
102111 } {
103− const workspaceId = url.searchParams.get("workspaceId") ?? undefined;
112+ const rawWorkspaceId = url.searchParams.get("workspaceId");
113+ const workspaceId = rawWorkspaceId ? parseWorkspaceId(rawWorkspaceId) : undefined;
104114 const actorId = url.searchParams.get("actorId") ?? undefined;
105115 const clientId = url.searchParams.get("clientId") ?? undefined;
106116 const sessionId = url.searchParams.get("sessionId") ?? undefined;
@@ -122,12 +132,12 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 15 unmodified lines
122132 timeoutMs?: number;
123133 runtimeId?: string;
124134 metadata?: Record<string, unknown>;
125− workspaceId: string;
135+ workspaceId: Id<"workspaces">;
126136 actorId: string;
127137 clientId?: string;
128138 }) => {
129139 const created = await convex.mutation(api.executor.createTask, {
130− workspaceId: input.workspaceId as Id<"workspaces">,
140+ workspaceId: input.workspaceId,
131141 sessionId,
132142 code: input.code,
133143 timeoutMs: input.timeoutMs,
@@ -139,13 +149,13 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 5 unmodified lines
139149 return created as { task: TaskRecord };
140150 },
141151
142− getTask: async (taskId: string, workspace?: string) => {
152+ getTask: async (taskId: string, workspace?: Id<"workspaces">) => {
143153 const effectiveWorkspaceId = workspace ?? workspaceId;
144154 if (!effectiveWorkspaceId) {
145155 return null;
146156 }
147157 const task = await convex.query(api.workspace.getTaskInWorkspace, {
148− workspaceId: effectiveWorkspaceId as Id<"workspaces">,
158+ workspaceId: effectiveWorkspaceId,
149159 sessionId,
150160 taskId,
151161 });
@@ -161,12 +171,12 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 9 unmodified lines
161171 return bootstrap as AnonymousContext;
162172 },
163173
164− listTools: async (toolContext?: { workspaceId: string; actorId?: string; clientId?: string }) => {
174+ listTools: async (toolContext?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => {
165175 if (!toolContext) {
166176 return [];
167177 }
168178 const tools = await convex.action(api.executorNode.listTools, {
169− workspaceId: toolContext.workspaceId as Id<"workspaces">,
179+ workspaceId: toolContext.workspaceId,
170180 sessionId,
171181 actorId: toolContext.actorId,
172182 clientId: toolContext.clientId,
@@ -174,23 +184,23 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 1 unmodified line
174184 return tools as ToolDescriptor[];
175185 },
176186
177− listPendingApprovals: async (approvalWorkspaceId: string) => {
187+ listPendingApprovals: async (approvalWorkspaceId: Id<"workspaces">) => {
178188 const approvals = await convex.query(api.workspace.listPendingApprovals, {
179− workspaceId: approvalWorkspaceId as Id<"workspaces">,
189+ workspaceId: approvalWorkspaceId,
180190 sessionId,
181191 });
182192 return approvals as PendingApprovalRecord[];
183193 },
184194
185195 resolveApproval: async (input: {
186− workspaceId: string;
196+ workspaceId: Id<"workspaces">;
187197 approvalId: string;
188198 decision: "approved" | "denied";
189199 reviewerId?: string;
190200 reason?: string;
191201 }) => {
192202 return await convex.mutation(api.executor.resolveApproval, {
193− workspaceId: input.workspaceId as Id<"workspaces">,
203+ workspaceId: input.workspaceId,
194204 sessionId,
195205 approvalId: input.approvalId,
196206 decision: input.decision,
@@ -203,7 +213,7 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 6 unmodified lines
203213
204214 async function resolveContext(
205215 requested: {
206− workspaceId?: string;
216+ workspaceId?: Id<"workspaces">;
207217 actorId?: string;
208218 clientId?: string;
209219 sessionId?: string;
@@ -233,7 +243,7 @@ async function resolveContext( · 23 unmodified lines
233243 }
234244
235245 const requestContext = await convex.query(api.workspace.getRequestContext, {
236− workspaceId: requested.workspaceId as Id<"workspaces">,
246+ workspaceId: requested.workspaceId,
237247 sessionId: requested.sessionId,
238248 });
239249