History

Tighten schema, delete @executor/contracts, add @executor/convex package

- Change workspaceId from v.string() to v.id('workspaces') in 7 tables and cascade the type fix through all validators, helpers, and call sites - Make anonymousSessions.userId and workspaceToolCache.dtsStorageIds required with cleanup/backfill migrations for both dev and prod - Delete pushSubscriptions table - Remove legacy OpenAPI compat (schemaTypes/operationTypes), legacy GraphQL run spec, and credential provider fallback code - Inline @executor/contracts types into lib/types.ts and web types.ts, delete the contracts package entirely - Turn executor/convex/ into @executor/convex workspace package so the web app imports from @executor/convex/_generated/* instead of fragile relative paths

Rhys Sullivan committed 9 months ago

Commit 1005f831 parent 552bff744 files · +561 −796Browse files

Unified · Split

44 files
  1. bun.lock +4 −5
  2. executor/apps/web/next.config.ts +1 −1
  3. executor/apps/web/package.json +1 −1
  4. executor/apps/web/src/app/sign-in/route.ts +2 −2
  5. executor/apps/web/src/app/sign-up/route.ts +2 −2
  6. executor/apps/web/src/components/code-editor.tsx +0 −16
  7. executor/apps/web/src/components/members-view.tsx +2 −2
  8. executor/apps/web/src/components/tasks-view.tsx +2 −1
  9. executor/apps/web/src/hooks/use-workspace-tools.ts +2 −1
  10. executor/apps/web/src/lib/convex-api.ts +1 −1
  11. executor/apps/web/src/lib/session-context.tsx +1 −1
  12. executor/apps/web/src/lib/types.ts +124 −22
  13. executor/convex/_generated/api.d.ts +72 −45
  14. executor/convex/access-controls.test.ts +31 −35
  15. executor/convex/accounts.ts +7 −9
  16. executor/convex/credentialsNode.ts +1 −1
  17. executor/convex/database.test.ts +35 −9
  18. executor/convex/database.ts +16 −16
  19. executor/convex/executor-mcp.e2e.test.ts +3 −2
  20. executor/convex/executor.ts +5 −4
  21. executor/convex/executorNode.ts +4 −6
  22. executor/convex/http.ts +15 −8
  23. executor/convex/migrations.ts +20 −0
  24. executor/convex/package.json +7 −0
  25. executor/convex/schema.ts +10 −25
  26. executor/convex/workspaceAuthInternal.ts +2 −2
  27. executor/convex/workspaceToolCache.ts +9 −11
  28. executor/convex/workspaces.ts +6 −8
  29. executor/lib/credential-providers.test.ts +0 −9
  30. executor/lib/credential_providers.ts +1 −9
  31. executor/lib/functionBuilders.ts +2 −2
  32. executor/lib/identity.ts +3 −3
  33. executor/lib/mcp-server.test.ts +2 −0
  34. executor/lib/openapi-cache.test.ts +0 −188
  35. executor/lib/tool_sources.ts +1 −28
  36. executor/lib/typechecker.test.ts +0 −93
  37. executor/lib/typechecker.ts +0 −19
  38. executor/lib/types.ts +122 −24
  39. executor/lib/workspace-tool-cache.test.ts +37 −17
  40. executor/mcp-gateway.ts +7 −6
  41. executor/packages/contracts/package.json +0 −7
  42. executor/packages/contracts/src/index.ts +0 −143
  43. executor/packages/contracts/tsconfig.json +0 −11
  44. package.json +1 −1
bun.lock+4−5
@@ -9,7 +9,6 @@ · 8 unmodified lines
9 "@convex-dev/migrations": "^0.3.1",9 "@convex-dev/migrations": "^0.3.1",
10 "@convex-dev/stripe": "^0.1.3",10 "@convex-dev/stripe": "^0.1.3",
11 "@convex-dev/workos-authkit": "^0.1.6",11 "@convex-dev/workos-authkit": "^0.1.6",
12 "@executor/contracts": "workspace:*",
13 "@workos-inc/node": "^8.2.0",12 "@workos-inc/node": "^8.2.0",
14 "convex": "^1.31.7",13 "convex": "^1.31.7",
15 "convex-helpers": "^0.1.111",14 "convex-helpers": "^0.1.111",
@@ -91,7 +90,7 @@ · 75 unmodified lines
91 "dependencies": {90 "dependencies": {
92 "@convex-dev/workos": "^0.0.1",91 "@convex-dev/workos": "^0.0.1",
93 "@elysiajs/eden": "^1.4.6",92 "@elysiajs/eden": "^1.4.6",
94 "@executor/contracts": "workspace:*",93 "@executor/convex": "workspace:*",
95 "@monaco-editor/react": "^4.7.0",94 "@monaco-editor/react": "^4.7.0",
96 "@streamdown/code": "^1.0.1",95 "@streamdown/code": "^1.0.1",
97 "@tanstack/react-query": "^5.80.7",96 "@tanstack/react-query": "^5.80.7",
@@ -127,8 +126,8 @@ · 29 unmodified lines
127 "typescript": "^5",126 "typescript": "^5",
128 },127 },
129 },128 },
130 "executor/packages/contracts": {129 "executor/convex": {
131 "name": "@executor/contracts",130 "name": "@executor/convex",
132 "version": "0.0.1",131 "version": "0.0.1",
133 },132 },
134 },133 },
@@ -399,7 +398,7 @@ · 264 unmodified lines
399 398
400 "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],399 "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
401 400
402 "@executor/contracts": ["@executor/contracts@workspace:executor/packages/contracts"],401 "@executor/convex": ["@executor/convex@workspace:executor/convex"],
403 402
404 "@executor/web": ["@executor/web@workspace:executor/apps/web"],403 "@executor/web": ["@executor/web@workspace:executor/apps/web"],
405 404
executor/apps/web/next.config.ts+1−1
@@ -13,7 +13,7 @@ const appShellRewriteExclusions = [ · 12 unmodified lines
13].join("|");13].join("|");
14 14
15const nextConfig: NextConfig = {15const nextConfig: NextConfig = {
16 transpilePackages: ["@executor/contracts"],16 transpilePackages: ["@executor/convex"],
17 env: {17 env: {
18 // Map canonical env vars to NEXT_PUBLIC_ so they're available client-side.18 // Map canonical env vars to NEXT_PUBLIC_ so they're available client-side.
19 // This lets us keep a single root .env without NEXT_PUBLIC_ prefixes.19 // This lets us keep a single root .env without NEXT_PUBLIC_ prefixes.
executor/apps/web/package.json+1−1
@@ -11,8 +11,8 @@ · 10 unmodified lines
11 },11 },
12 "dependencies": {12 "dependencies": {
13 "@convex-dev/workos": "^0.0.1",13 "@convex-dev/workos": "^0.0.1",
14 "@executor/convex": "workspace:*",
14 "@elysiajs/eden": "^1.4.6",15 "@elysiajs/eden": "^1.4.6",
15 "@executor/contracts": "workspace:*",
16 "@monaco-editor/react": "^4.7.0",16 "@monaco-editor/react": "^4.7.0",
17 "@streamdown/code": "^1.0.1",17 "@streamdown/code": "^1.0.1",
18 "@tanstack/react-query": "^5.80.7",18 "@tanstack/react-query": "^5.80.7",
executor/apps/web/src/app/sign-in/route.ts+2−2
@@ -1,8 +1,8 @@
1import type { NextRequest } from "next/server";1import type { NextRequest } from "next/server";
2import { redirect } from "next/navigation";2import { redirect } from "next/navigation";
3import { ConvexHttpClient } from "convex/browser";3import { ConvexHttpClient } from "convex/browser";
4import { api } from "../../../../../convex/_generated/api";4import { api } from "@executor/convex/_generated/api";
5import type { Id } from "../../../../../convex/_generated/dataModel";5import type { Id } from "@executor/convex/_generated/dataModel";
6 6
7function getExternalOrigin(request: NextRequest) {7function getExternalOrigin(request: NextRequest) {
8 const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host");8 const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host");
executor/apps/web/src/app/sign-up/route.ts+2−2
@@ -1,8 +1,8 @@
1import type { NextRequest } from "next/server";1import type { NextRequest } from "next/server";
2import { redirect } from "next/navigation";2import { redirect } from "next/navigation";
3import { ConvexHttpClient } from "convex/browser";3import { ConvexHttpClient } from "convex/browser";
4import { api } from "../../../../../convex/_generated/api";4import { api } from "@executor/convex/_generated/api";
5import type { Id } from "../../../../../convex/_generated/dataModel";5import type { Id } from "@executor/convex/_generated/dataModel";
6 6
7function getExternalOrigin(request: NextRequest) {7function getExternalOrigin(request: NextRequest) {
8 const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host");8 const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host");
executor/apps/web/src/components/code-editor.tsx+0−16
@@ -131,17 +131,6 @@ type ToolOutput<Op> = · 130 unmodified lines
131function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string>): string {131function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string>): string {
132 const root = buildTree(tools);132 const root = buildTree(tools);
133 133
134 // Legacy compat: collect schemaTypes from tools that use the old format
135 const schemaTypeAliases = new Map<string, string>();
136 for (const tool of tools) {
137 if (!tool.schemaTypes) continue;
138 for (const [name, type] of Object.entries(tool.schemaTypes)) {
139 if (!schemaTypeAliases.has(name)) {
140 schemaTypeAliases.set(name, type);
141 }
142 }
143 }
144
145 const interfaces: string[] = [];134 const interfaces: string[] = [];
146 135
147 // Emit all namespace interfaces recursively136 // Emit all namespace interfaces recursively
@@ -170,11 +159,6 @@ function generateToolsDts(tools: ToolDescriptor[], dtsSources: Set<string>): str · 22 unmodified lines
170 */159 */
171`;160`;
172 161
173 // Legacy schema type aliases
174 if (schemaTypeAliases.size > 0) {
175 dts += Array.from(schemaTypeAliases, ([name, type]) => `type ${name} = ${type};`).join("\n") + "\n\n";
176 }
177
178 // Note: .d.ts blocks from OpenAPI sources are fetched separately and registered162 // Note: .d.ts blocks from OpenAPI sources are fetched separately and registered
179 // as a distinct Monaco extra lib (see dtsUrls effect). This keeps generateToolsDts163 // as a distinct Monaco extra lib (see dtsUrls effect). This keeps generateToolsDts
180 // fast and avoids bundling multi-MB .d.ts content into the tool declarations string.164 // fast and avoids bundling multi-MB .d.ts content into the tool declarations string.
executor/apps/web/src/components/members-view.tsx+2−2
@@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; · 8 unmodified lines
9import { Input } from "@/components/ui/input";9import { Input } from "@/components/ui/input";
10import { useSession } from "@/lib/session-context";10import { useSession } from "@/lib/session-context";
11import { convexApi } from "@/lib/convex-api";11import { convexApi } from "@/lib/convex-api";
12import type { Id } from "../../../../convex/_generated/dataModel";12import type { Id } from "@executor/convex/_generated/dataModel";
13 13
14type Role = "owner" | "admin" | "member" | "billing_admin";14type Role = "owner" | "admin" | "member" | "billing_admin";
15type MemberStatus = "active" | "pending" | "removed";15type MemberStatus = "active" | "pending" | "removed";
@@ -64,7 +64,7 @@ export function MembersView({ showHeader = true }: MembersViewProps) { · 48 unmodified lines
64 64
65 const memberItems: OrganizationMemberListItem[] = members?.items ?? [];65 const memberItems: OrganizationMemberListItem[] = members?.items ?? [];
66 const actorMembership = memberItems.find((member: { accountId?: string; role?: string }) =>66 const actorMembership = memberItems.find((member: { accountId?: string; role?: string }) =>
67 context?.accountId ? member.accountId === context.accountId : false,67 context ? member.accountId === context.accountId : false,
68 );68 );
69 const actorRole = actorMembership?.role ?? null;69 const actorRole = actorMembership?.role ?? null;
70 const canManageMembers = actorRole === "owner" || actorRole === "admin";70 const canManageMembers = actorRole === "owner" || actorRole === "admin";
executor/apps/web/src/components/tasks-view.tsx+2−1
@@ -34,6 +34,7 @@ import type { · 33 unmodified lines
34 TaskEventRecord,34 TaskEventRecord,
35 TaskRecord,35 TaskRecord,
36} from "@/lib/types";36} from "@/lib/types";
37import type { Id } from "@executor/convex/_generated/dataModel";
37import { toast } from "sonner";38import { toast } from "sonner";
38import { cn } from "@/lib/utils";39import { cn } from "@/lib/utils";
39 40
@@ -210,7 +211,7 @@ function TaskDetail({ · 170 unmodified lines
210 onClose,211 onClose,
211}: {212}: {
212 task: TaskRecord;213 task: TaskRecord;
213 workspaceId: string;214 workspaceId: Id<"workspaces">;
214 sessionId?: string;215 sessionId?: string;
215 onClose: () => void;216 onClose: () => void;
216}) {217}) {
executor/apps/web/src/hooks/use-workspace-tools.ts+2−1
@@ -4,9 +4,10 @@ import { useQuery as useTanstackQuery } from "@tanstack/react-query"; · 3 unmodified lines
4import { useAction, useQuery as useConvexQuery } from "convex/react";4import { useAction, useQuery as useConvexQuery } from "convex/react";
5import { convexApi } from "@/lib/convex-api";5import { convexApi } from "@/lib/convex-api";
6import type { OpenApiSourceQuality, ToolDescriptor } from "@/lib/types";6import type { OpenApiSourceQuality, ToolDescriptor } from "@/lib/types";
7import type { Id } from "@executor/convex/_generated/dataModel";
7 8
8interface WorkspaceContext {9interface WorkspaceContext {
9 workspaceId: string;10 workspaceId: Id<"workspaces">;
10 actorId?: string;11 actorId?: string;
11 clientId?: string;12 clientId?: string;
12 sessionId?: string;13 sessionId?: string;
executor/apps/web/src/lib/convex-api.ts+1−1
@@ -1,3 +1,3 @@
1import { api } from "../../../../convex/_generated/api";1import { api } from "@executor/convex/_generated/api";
2 2
3export const convexApi = api;3export const convexApi = api;
executor/apps/web/src/lib/session-context.tsx+1−1
@@ -14,7 +14,7 @@ import { anonymousDemoEnabled, workosEnabled } from "@/lib/auth-capabilities"; · 13 unmodified lines
14import { useWorkosAuthLoading } from "@/lib/convex-provider";14import { useWorkosAuthLoading } from "@/lib/convex-provider";
15import { convexApi } from "@/lib/convex-api";15import { convexApi } from "@/lib/convex-api";
16import type { AnonymousContext } from "./types";16import type { AnonymousContext } from "./types";
17import type { Id } from "../../../../convex/_generated/dataModel";17import type { Id } from "@executor/convex/_generated/dataModel";
18 18
19interface SessionState {19interface SessionState {
20 context: AnonymousContext | null;20 context: AnonymousContext | null;
executor/apps/web/src/lib/types.ts+124−22
@@ -1,27 +1,129 @@
1// Re-export shared types from contracts1import type { Id } from "@executor/convex/_generated/dataModel";
2export type {
3 TaskStatus,
4 ApprovalStatus,
5 PolicyDecision,
6 CredentialScope,
7 CredentialProvider,
8 ToolApprovalMode,
9 ToolSourceType,
10 TaskRecord,
11 ApprovalRecord,
12 PendingApprovalRecord,
13 TaskEventRecord,
14 AccessPolicyRecord,
15 CredentialRecord,
16 ToolSourceRecord,
17 ToolDescriptor,
18 OpenApiSourceQuality,
19 AnonymousContext,
20} from "@executor/contracts";
21 2
22// ── Web-only types ────────────────────────────────────────────────────────────3// ── Shared types (inlined from @executor/contracts) ──────────────────────────
4
5export type TaskStatus = "queued" | "running" | "completed" | "failed" | "timed_out" | "denied";
6export type ApprovalStatus = "pending" | "approved" | "denied";
7export type PolicyDecision = "allow" | "require_approval" | "deny";
8export type CredentialScope = "workspace" | "actor";
9export type CredentialProvider = "managed" | "workos-vault";
10export type ToolApprovalMode = "auto" | "required";
11export type ToolSourceType = "mcp" | "openapi" | "graphql";
12
13export interface TaskRecord {
14 id: string;
15 code: string;
16 runtimeId: string;
17 status: TaskStatus;
18 timeoutMs: number;
19 metadata: Record<string, unknown>;
20 workspaceId: string;
21 actorId?: string;
22 clientId?: string;
23 createdAt: number;
24 updatedAt: number;
25 startedAt?: number;
26 completedAt?: number;
27 error?: string;
28 stdout?: string;
29 stderr?: string;
30 exitCode?: number;
31}
32
33export interface ApprovalRecord {
34 id: string;
35 taskId: string;
36 toolPath: string;
37 input: unknown;
38 status: ApprovalStatus;
39 reason?: string;
40 reviewerId?: string;
41 createdAt: number;
42 resolvedAt?: number;
43}
44
45export interface PendingApprovalRecord extends ApprovalRecord {
46 task: Pick<TaskRecord, "id" | "status" | "runtimeId" | "timeoutMs" | "createdAt">;
47}
48
49export interface TaskEventRecord {
50 id: number;
51 taskId: string;
52 eventName: string;
53 type: string;
54 payload: unknown;
55 createdAt: number;
56}
57
58export interface AccessPolicyRecord {
59 id: string;
60 workspaceId: string;
61 actorId?: string;
62 clientId?: string;
63 toolPathPattern: string;
64 decision: PolicyDecision;
65 priority: number;
66 createdAt: number;
67 updatedAt: number;
68}
69
70export interface CredentialRecord {
71 id: string;
72 workspaceId: string;
73 sourceKey: string;
74 scope: CredentialScope;
75 actorId?: string;
76 provider: CredentialProvider;
77 secretJson: Record<string, unknown>;
78 createdAt: number;
79 updatedAt: number;
80}
81
82export interface ToolSourceRecord {
83 id: string;
84 workspaceId: string;
85 name: string;
86 type: ToolSourceType;
87 config: Record<string, unknown>;
88 enabled: boolean;
89 createdAt: number;
90 updatedAt: number;
91}
92
93export interface ToolDescriptor {
94 path: string;
95 description: string;
96 approval: ToolApprovalMode;
97 source?: string;
98 argsType?: string;
99 returnsType?: string;
100 operationId?: string;
101}
102
103export interface OpenApiSourceQuality {
104 sourceKey: string;
105 toolCount: number;
106 unknownArgsCount: number;
107 unknownReturnsCount: number;
108 partialUnknownArgsCount: number;
109 partialUnknownReturnsCount: number;
110 argsQuality: number;
111 returnsQuality: number;
112 overallQuality: number;
113}
114
115export interface AnonymousContext {
116 sessionId: string;
117 workspaceId: Id<"workspaces">;
118 actorId: string;
119 clientId: string;
120 accountId: string;
121 userId: string;
122 createdAt: number;
123 lastSeenAt: number;
124}
23 125
24import type { TaskStatus, ApprovalStatus, CredentialScope } from "@executor/contracts";126// ── Web-only types ────────────────────────────────────────────────────────────
25 127
26export type ApprovalDecision = "approved" | "denied";128export type ApprovalDecision = "approved" | "denied";
27 129
executor/convex/_generated/api.d.ts+72−45
@@ -93,7 +93,7 @@ export declare const api: { · 92 unmodified lines
93 secretJson: any;93 secretJson: any;
94 sessionId?: string;94 sessionId?: string;
95 sourceKey: string;95 sourceKey: string;
96 workspaceId: string;96 workspaceId: Id<"workspaces">;
97 },97 },
98 any98 any
99 >;99 >;
@@ -107,7 +107,7 @@ export declare const api: { · 7 unmodified lines
107 id: string;107 id: string;
108 prompt: string;108 prompt: string;
109 requesterId: string;109 requesterId: string;
110 workspaceId: string;110 workspaceId: Id<"workspaces">;
111 },111 },
112 any112 any
113 >;113 >;
@@ -142,7 +142,7 @@ export declare const api: { · 28 unmodified lines
142 runtimeId?: string;142 runtimeId?: string;
143 sessionId?: string;143 sessionId?: string;
144 timeoutMs?: number;144 timeoutMs?: number;
145 workspaceId: string;145 workspaceId: Id<"workspaces">;
146 },146 },
147 any147 any
148 >;148 >;
@@ -155,7 +155,7 @@ export declare const api: { · 6 unmodified lines
155 reason?: string;155 reason?: string;
156 reviewerId?: string;156 reviewerId?: string;
157 sessionId?: string;157 sessionId?: string;
158 workspaceId: string;158 workspaceId: Id<"workspaces">;
159 },159 },
160 any160 any
161 >;161 >;
@@ -168,7 +168,7 @@ export declare const api: { · 6 unmodified lines
168 actorId?: string;168 actorId?: string;
169 clientId?: string;169 clientId?: string;
170 sessionId?: string;170 sessionId?: string;
171 workspaceId: string;171 workspaceId: Id<"workspaces">;
172 },172 },
173 any173 any
174 >;174 >;
@@ -179,7 +179,7 @@ export declare const api: { · 4 unmodified lines
179 actorId?: string;179 actorId?: string;
180 clientId?: string;180 clientId?: string;
181 sessionId?: string;181 sessionId?: string;
182 workspaceId: string;182 workspaceId: Id<"workspaces">;
183 },183 },
184 any184 any
185 >;185 >;
@@ -292,31 +292,31 @@ export declare const api: { · 106 unmodified lines
292 deleteToolSource: FunctionReference<292 deleteToolSource: FunctionReference<
293 "mutation",293 "mutation",
294 "public",294 "public",
295 { sessionId?: string; sourceId: string; workspaceId: string },295 { sessionId?: string; sourceId: string; workspaceId: Id<"workspaces"> },
296 any296 any
297 >;297 >;
298 getRequestContext: FunctionReference<298 getRequestContext: FunctionReference<
299 "query",299 "query",
300 "public",300 "public",
301 { sessionId?: string; workspaceId: string },301 { sessionId?: string; workspaceId: Id<"workspaces"> },
302 any302 any
303 >;303 >;
304 getTask: FunctionReference<304 getTask: FunctionReference<
305 "query",305 "query",
306 "public",306 "public",
307 { sessionId?: string; taskId: string; workspaceId: string },307 { sessionId?: string; taskId: string; workspaceId: Id<"workspaces"> },
308 any308 any
309 >;309 >;
310 getTaskInWorkspace: FunctionReference<310 getTaskInWorkspace: FunctionReference<
311 "query",311 "query",
312 "public",312 "public",
313 { sessionId?: string; taskId: string; workspaceId: string },313 { sessionId?: string; taskId: string; workspaceId: Id<"workspaces"> },
314 any314 any
315 >;315 >;
316 listAccessPolicies: FunctionReference<316 listAccessPolicies: FunctionReference<
317 "query",317 "query",
318 "public",318 "public",
319 { sessionId?: string; workspaceId: string },319 { sessionId?: string; workspaceId: Id<"workspaces"> },
320 any320 any
321 >;321 >;
322 listApprovals: FunctionReference<322 listApprovals: FunctionReference<
@@ -325,7 +325,7 @@ export declare const api: { · 2 unmodified lines
325 {325 {
326 sessionId?: string;326 sessionId?: string;
327 status?: "pending" | "approved" | "denied";327 status?: "pending" | "approved" | "denied";
328 workspaceId: string;328 workspaceId: Id<"workspaces">;
329 },329 },
330 any330 any
331 >;331 >;
@@ -333,32 +333,32 @@ export declare const api: { · 1 unmodified line
333 listCredentials: FunctionReference<333 listCredentials: FunctionReference<
334 "query",334 "query",
335 "public",335 "public",
336 { sessionId?: string; workspaceId: string },336 { sessionId?: string; workspaceId: Id<"workspaces"> },
337 any337 any
338 >;338 >;
339 listPendingApprovals: FunctionReference<339 listPendingApprovals: FunctionReference<
340 "query",340 "query",
341 "public",341 "public",
342 { sessionId?: string; workspaceId: string },342 { sessionId?: string; workspaceId: Id<"workspaces"> },
343 any343 any
344 >;344 >;
345 listRuntimeTargets: FunctionReference<"query", "public", {}, any>;345 listRuntimeTargets: FunctionReference<"query", "public", {}, any>;
346 listTaskEvents: FunctionReference<346 listTaskEvents: FunctionReference<
347 "query",347 "query",
348 "public",348 "public",
349 { sessionId?: string; taskId: string; workspaceId: string },349 { sessionId?: string; taskId: string; workspaceId: Id<"workspaces"> },
350 any350 any
351 >;351 >;
352 listTasks: FunctionReference<352 listTasks: FunctionReference<
353 "query",353 "query",
354 "public",354 "public",
355 { sessionId?: string; workspaceId: string },355 { sessionId?: string; workspaceId: Id<"workspaces"> },
356 any356 any
357 >;357 >;
358 listToolSources: FunctionReference<358 listToolSources: FunctionReference<
359 "query",359 "query",
360 "public",360 "public",
361 { sessionId?: string; workspaceId: string },361 { sessionId?: string; workspaceId: Id<"workspaces"> },
362 any362 any
363 >;363 >;
364 resolveCredential: FunctionReference<364 resolveCredential: FunctionReference<
@@ -369,7 +369,7 @@ export declare const api: { · 4 unmodified lines
369 scope: "workspace" | "actor";369 scope: "workspace" | "actor";
370 sessionId?: string;370 sessionId?: string;
371 sourceKey: string;371 sourceKey: string;
372 workspaceId: string;372 workspaceId: Id<"workspaces">;
373 },373 },
374 any374 any
375 >;375 >;
@@ -384,7 +384,7 @@ export declare const api: { · 8 unmodified lines
384 priority?: number;384 priority?: number;
385 sessionId?: string;385 sessionId?: string;
386 toolPathPattern: string;386 toolPathPattern: string;
387 workspaceId: string;387 workspaceId: Id<"workspaces">;
388 },388 },
389 any389 any
390 >;390 >;
@@ -399,7 +399,7 @@ export declare const api: { · 8 unmodified lines
399 secretJson: any;399 secretJson: any;
400 sessionId?: string;400 sessionId?: string;
401 sourceKey: string;401 sourceKey: string;
402 workspaceId: string;402 workspaceId: Id<"workspaces">;
403 },403 },
404 any404 any
405 >;405 >;
@@ -413,7 +413,7 @@ export declare const api: { · 7 unmodified lines
413 name: string;413 name: string;
414 sessionId?: string;414 sessionId?: string;
415 type: "mcp" | "openapi" | "graphql";415 type: "mcp" | "openapi" | "graphql";
416 workspaceId: string;416 workspaceId: Id<"workspaces">;
417 },417 },
418 any418 any
419 >;419 >;
@@ -532,7 +532,7 @@ export declare const internal: { · 112 unmodified lines
532 metadata?: any;532 metadata?: any;
533 runtimeId: string;533 runtimeId: string;
534 timeoutMs?: number;534 timeoutMs?: number;
535 workspaceId: string;535 workspaceId: Id<"workspaces">;
536 },536 },
537 any537 any
538 >;538 >;
@@ -545,7 +545,7 @@ export declare const internal: { · 6 unmodified lines
545 deleteToolSource: FunctionReference<545 deleteToolSource: FunctionReference<
546 "mutation",546 "mutation",
547 "internal",547 "internal",
548 { sourceId: string; workspaceId: string },548 { sourceId: string; workspaceId: Id<"workspaces"> },
549 any549 any
550 >;550 >;
551 getApproval: FunctionReference<551 getApproval: FunctionReference<
@@ -557,39 +557,42 @@ export declare const internal: { · 5 unmodified lines
557 getApprovalInWorkspace: FunctionReference<557 getApprovalInWorkspace: FunctionReference<
558 "query",558 "query",
559 "internal",559 "internal",
560 { approvalId: string; workspaceId: string },560 { approvalId: string; workspaceId: Id<"workspaces"> },
561 any561 any
562 >;562 >;
563 getTask: FunctionReference<"query", "internal", { taskId: string }, any>;563 getTask: FunctionReference<"query", "internal", { taskId: string }, any>;
564 getTaskInWorkspace: FunctionReference<564 getTaskInWorkspace: FunctionReference<
565 "query",565 "query",
566 "internal",566 "internal",
567 { taskId: string; workspaceId: string },567 { taskId: string; workspaceId: Id<"workspaces"> },
568 any568 any
569 >;569 >;
570 listAccessPolicies: FunctionReference<570 listAccessPolicies: FunctionReference<
571 "query",571 "query",
572 "internal",572 "internal",
573 { workspaceId: string },573 { workspaceId: Id<"workspaces"> },
574 any574 any
575 >;575 >;
576 listApprovals: FunctionReference<576 listApprovals: FunctionReference<
577 "query",577 "query",
578 "internal",578 "internal",
579 { status?: "pending" | "approved" | "denied"; workspaceId: string },579 {
580 status?: "pending" | "approved" | "denied";
581 workspaceId: Id<"workspaces">;
582 },
580 any583 any
581 >;584 >;
582 listCredentialProviders: FunctionReference<"query", "internal", {}, any>;585 listCredentialProviders: FunctionReference<"query", "internal", {}, any>;
583 listCredentials: FunctionReference<586 listCredentials: FunctionReference<
584 "query",587 "query",
585 "internal",588 "internal",
586 { workspaceId: string },589 { workspaceId: Id<"workspaces"> },
587 any590 any
588 >;591 >;
589 listPendingApprovals: FunctionReference<592 listPendingApprovals: FunctionReference<
590 "query",593 "query",
591 "internal",594 "internal",
592 { workspaceId: string },595 { workspaceId: Id<"workspaces"> },
593 any596 any
594 >;597 >;
595 listQueuedTaskIds: FunctionReference<598 listQueuedTaskIds: FunctionReference<
@@ -608,13 +611,13 @@ export declare const internal: { · 12 unmodified lines
608 listTasks: FunctionReference<611 listTasks: FunctionReference<
609 "query",612 "query",
610 "internal",613 "internal",
611 { workspaceId: string },614 { workspaceId: Id<"workspaces"> },
612 any615 any
613 >;616 >;
614 listToolSources: FunctionReference<617 listToolSources: FunctionReference<
615 "query",618 "query",
616 "internal",619 "internal",
617 { workspaceId: string },620 { workspaceId: Id<"workspaces"> },
618 any621 any
619 >;622 >;
620 markTaskFinished: FunctionReference<623 markTaskFinished: FunctionReference<
@@ -654,7 +657,7 @@ export declare const internal: { · 33 unmodified lines
654 actorId?: string;657 actorId?: string;
655 scope: "workspace" | "actor";658 scope: "workspace" | "actor";
656 sourceKey: string;659 sourceKey: string;
657 workspaceId: string;660 workspaceId: Id<"workspaces">;
658 },661 },
659 any662 any
660 >;663 >;
@@ -668,7 +671,7 @@ export declare const internal: { · 7 unmodified lines
668 id?: string;671 id?: string;
669 priority?: number;672 priority?: number;
670 toolPathPattern: string;673 toolPathPattern: string;
671 workspaceId: string;674 workspaceId: Id<"workspaces">;
672 },675 },
673 any676 any
674 >;677 >;
@@ -682,7 +685,7 @@ export declare const internal: { · 7 unmodified lines
682 scope: "workspace" | "actor";685 scope: "workspace" | "actor";
683 secretJson: any;686 secretJson: any;
684 sourceKey: string;687 sourceKey: string;
685 workspaceId: string;688 workspaceId: Id<"workspaces">;
686 },689 },
687 any690 any
688 >;691 >;
@@ -695,7 +698,7 @@ export declare const internal: { · 6 unmodified lines
695 id?: string;698 id?: string;
696 name: string;699 name: string;
697 type: "mcp" | "openapi" | "graphql";700 type: "mcp" | "openapi" | "graphql";
698 workspaceId: string;701 workspaceId: Id<"workspaces">;
699 },702 },
700 any703 any
701 >;704 >;
@@ -722,7 +725,7 @@ export declare const internal: { · 20 unmodified lines
722 metadata?: any;725 metadata?: any;
723 runtimeId?: string;726 runtimeId?: string;
724 timeoutMs?: number;727 timeoutMs?: number;
725 workspaceId: string;728 workspaceId: Id<"workspaces">;
726 },729 },
727 any730 any
728 >;731 >;
@@ -734,7 +737,7 @@ export declare const internal: { · 5 unmodified lines
734 decision: "approved" | "denied";737 decision: "approved" | "denied";
735 reason?: string;738 reason?: string;
736 reviewerId?: string;739 reviewerId?: string;
737 workspaceId: string;740 workspaceId: Id<"workspaces">;
738 },741 },
739 any742 any
740 >;743 >;
@@ -749,13 +752,13 @@ export declare const internal: { · 8 unmodified lines
749 listToolsInternal: FunctionReference<752 listToolsInternal: FunctionReference<
750 "action",753 "action",
751 "internal",754 "internal",
752 { actorId?: string; clientId?: string; workspaceId: string },755 { actorId?: string; clientId?: string; workspaceId: Id<"workspaces"> },
753 any756 any
754 >;757 >;
755 listToolsWithWarningsInternal: FunctionReference<758 listToolsWithWarningsInternal: FunctionReference<
756 "action",759 "action",
757 "internal",760 "internal",
758 { actorId?: string; clientId?: string; workspaceId: string },761 { actorId?: string; clientId?: string; workspaceId: Id<"workspaces"> },
759 any762 any
760 >;763 >;
761 runTask: FunctionReference<"action", "internal", { taskId: string }, any>;764 runTask: FunctionReference<"action", "internal", { taskId: string }, any>;
@@ -814,6 +817,18 @@ export declare const internal: { · 52 unmodified lines
814 >;817 >;
815 };818 };
816 migrations: {819 migrations: {
820 backfillDtsStorageIds: FunctionReference<
821 "mutation",
822 "internal",
823 {
824 batchSize?: number;
825 cursor?: string | null;
826 dryRun?: boolean;
827 fn?: string;
828 next?: Array<string>;
829 },
830 any
831 >;
817 deleteAnonymousSessionsMissingAccountId: FunctionReference<832 deleteAnonymousSessionsMissingAccountId: FunctionReference<
818 "mutation",833 "mutation",
819 "internal",834 "internal",
@@ -826,6 +841,18 @@ export declare const internal: { · 6 unmodified lines
826 },841 },
827 any842 any
828 >;843 >;
844 deleteAnonymousSessionsMissingUserId: 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 >;
829 deleteInvitesMissingProvider: FunctionReference<856 deleteInvitesMissingProvider: FunctionReference<
830 "mutation",857 "mutation",
831 "internal",858 "internal",
@@ -916,13 +943,13 @@ export declare const internal: { · 84 unmodified lines
916 getWorkspaceAccessForRequest: FunctionReference<943 getWorkspaceAccessForRequest: FunctionReference<
917 "query",944 "query",
918 "internal",945 "internal",
919 { sessionId?: string; workspaceId: string },946 { sessionId?: string; workspaceId: Id<"workspaces"> },
920 any947 any
921 >;948 >;
922 getWorkspaceAccessForWorkosSubject: FunctionReference<949 getWorkspaceAccessForWorkosSubject: FunctionReference<
923 "query",950 "query",
924 "internal",951 "internal",
925 { subject: string; workspaceId: string },952 { subject: string; workspaceId: Id<"workspaces"> },
926 any953 any
927 >;954 >;
928 };955 };
@@ -930,25 +957,25 @@ export declare const internal: { · 1 unmodified line
930 getDtsStorageIds: FunctionReference<957 getDtsStorageIds: FunctionReference<
931 "query",958 "query",
932 "internal",959 "internal",
933 { workspaceId: string },960 { workspaceId: Id<"workspaces"> },
934 any961 any
935 >;962 >;
936 getEntry: FunctionReference<963 getEntry: FunctionReference<
937 "query",964 "query",
938 "internal",965 "internal",
939 { signature: string; workspaceId: string },966 { signature: string; workspaceId: Id<"workspaces"> },
940 any967 any
941 >;968 >;
942 putEntry: FunctionReference<969 putEntry: FunctionReference<
943 "mutation",970 "mutation",
944 "internal",971 "internal",
945 {972 {
946 dtsStorageIds?: Array<{ sourceKey: string; storageId: Id<"_storage"> }>;973 dtsStorageIds: Array<{ sourceKey: string; storageId: Id<"_storage"> }>;
947 signature: string;974 signature: string;
948 sizeBytes: number;975 sizeBytes: number;
949 storageId: Id<"_storage">;976 storageId: Id<"_storage">;
950 toolCount: number;977 toolCount: number;
951 workspaceId: string;978 workspaceId: Id<"workspaces">;
952 },979 },
953 any980 any
954 >;981 >;
executor/convex/access-controls.test.ts+31−35
@@ -153,10 +153,6 @@ async function addWorkspaceMember( · 152 unmodified lines
153 });153 });
154}154}
155 155
156function asWorkspaceId(id: Id<"workspaces">): string {
157 return id as unknown as string;
158}
159
160// ---------------------------------------------------------------------------156// ---------------------------------------------------------------------------
161// Tests157// Tests
162// ---------------------------------------------------------------------------158// ---------------------------------------------------------------------------
@@ -168,7 +164,7 @@ describe("authentication", () => { · 5 unmodified lines
168 164
169 await expect(165 await expect(
170 t.query(api.workspace.listTasks, {166 t.query(api.workspace.listTasks, {
171 workspaceId: asWorkspaceId(owner.workspaceId),167 workspaceId: owner.workspaceId,
172 }),168 }),
173 ).rejects.toThrow("Must be signed in");169 ).rejects.toThrow("Must be signed in");
174 });170 });
@@ -179,7 +175,7 @@ describe("authentication", () => { · 4 unmodified lines
179 175
180 await expect(176 await expect(
181 t.mutation(api.workspace.upsertAccessPolicy, {177 t.mutation(api.workspace.upsertAccessPolicy, {
182 workspaceId: asWorkspaceId(owner.workspaceId),178 workspaceId: owner.workspaceId,
183 toolPathPattern: "*",179 toolPathPattern: "*",
184 decision: "allow",180 decision: "allow",
185 }),181 }),
@@ -203,7 +199,7 @@ describe("authentication", () => { · 17 unmodified lines
203 199
204 await expect(200 await expect(
205 t.mutation(api.executor.createTask, {201 t.mutation(api.executor.createTask, {
206 workspaceId: asWorkspaceId(owner.workspaceId),202 workspaceId: owner.workspaceId,
207 code: "console.log('hello')",203 code: "console.log('hello')",
208 }),204 }),
209 ).rejects.toThrow("Must be signed in");205 ).rejects.toThrow("Must be signed in");
@@ -215,7 +211,7 @@ describe("authentication", () => { · 5 unmodified lines
215 const authed = t.withIdentity({ subject: "user-ok" });211 const authed = t.withIdentity({ subject: "user-ok" });
216 212
217 const tasks = await authed.query(api.workspace.listTasks, {213 const tasks = await authed.query(api.workspace.listTasks, {
218 workspaceId: asWorkspaceId(owner.workspaceId),214 workspaceId: owner.workspaceId,
219 });215 });
220 216
221 expect(tasks).toEqual([]);217 expect(tasks).toEqual([]);
@@ -259,7 +255,7 @@ describe("workspace access controls", () => { · 37 unmodified lines
259 255
260 const authedMember = t.withIdentity({ subject: "ws-member" });256 const authedMember = t.withIdentity({ subject: "ws-member" });
261 const tasks = await authedMember.query(api.workspace.listTasks, {257 const tasks = await authedMember.query(api.workspace.listTasks, {
262 workspaceId: asWorkspaceId(owner.workspaceId),258 workspaceId: owner.workspaceId,
263 });259 });
264 260
265 expect(tasks).toEqual([]);261 expect(tasks).toEqual([]);
@@ -278,7 +274,7 @@ describe("workspace access controls", () => { · 12 unmodified lines
278 274
279 const authedMember = t.withIdentity({ subject: "ws-member-org-fallback" });275 const authedMember = t.withIdentity({ subject: "ws-member-org-fallback" });
280 const tasks = await authedMember.query(api.workspace.listTasks, {276 const tasks = await authedMember.query(api.workspace.listTasks, {
281 workspaceId: asWorkspaceId(owner.workspaceId),277 workspaceId: owner.workspaceId,
282 });278 });
283 279
284 expect(tasks).toEqual([]);280 expect(tasks).toEqual([]);
@@ -293,7 +289,7 @@ describe("workspace access controls", () => { · 8 unmodified lines
293 289
294 await expect(290 await expect(
295 authedOutsider.query(api.workspace.listTasks, {291 authedOutsider.query(api.workspace.listTasks, {
296 workspaceId: asWorkspaceId(owner.workspaceId),292 workspaceId: owner.workspaceId,
297 }),293 }),
298 ).rejects.toThrow("You are not a member of this workspace");294 ).rejects.toThrow("You are not a member of this workspace");
299 });295 });
@@ -319,7 +315,7 @@ describe("workspace access controls", () => { · 19 unmodified lines
319 315
320 await expect(316 await expect(
321 authedRemoved.query(api.workspace.listTasks, {317 authedRemoved.query(api.workspace.listTasks, {
322 workspaceId: asWorkspaceId(owner.workspaceId),318 workspaceId: owner.workspaceId,
323 }),319 }),
324 ).rejects.toThrow("You are not a member of this workspace");320 ).rejects.toThrow("You are not a member of this workspace");
325 });321 });
@@ -344,7 +340,7 @@ describe("workspace access controls", () => { · 18 unmodified lines
344 340
345 await expect(341 await expect(
346 authedMember.mutation(api.workspace.upsertAccessPolicy, {342 authedMember.mutation(api.workspace.upsertAccessPolicy, {
347 workspaceId: asWorkspaceId(owner.workspaceId),343 workspaceId: owner.workspaceId,
348 toolPathPattern: "*",344 toolPathPattern: "*",
349 decision: "allow",345 decision: "allow",
350 }),346 }),
@@ -370,7 +366,7 @@ describe("workspace access controls", () => { · 19 unmodified lines
370 const authedAdmin = t.withIdentity({ subject: "ws-policy-admin" });366 const authedAdmin = t.withIdentity({ subject: "ws-policy-admin" });
371 367
372 const policy = await authedAdmin.mutation(api.workspace.upsertAccessPolicy, {368 const policy = await authedAdmin.mutation(api.workspace.upsertAccessPolicy, {
373 workspaceId: asWorkspaceId(owner.workspaceId),369 workspaceId: owner.workspaceId,
374 toolPathPattern: "*.delete",370 toolPathPattern: "*.delete",
375 decision: "require_approval",371 decision: "require_approval",
376 });372 });
@@ -385,7 +381,7 @@ describe("workspace access controls", () => { · 8 unmodified lines
385 const authedOwner = t.withIdentity({ subject: "ws-owner-policy" });381 const authedOwner = t.withIdentity({ subject: "ws-owner-policy" });
386 382
387 const policy = await authedOwner.mutation(api.workspace.upsertAccessPolicy, {383 const policy = await authedOwner.mutation(api.workspace.upsertAccessPolicy, {
388 workspaceId: asWorkspaceId(owner.workspaceId),384 workspaceId: owner.workspaceId,
389 toolPathPattern: "*",385 toolPathPattern: "*",
390 decision: "allow",386 decision: "allow",
391 });387 });
@@ -413,7 +409,7 @@ describe("workspace access controls", () => { · 21 unmodified lines
413 409
414 await expect(410 await expect(
415 authedMember.mutation(api.workspace.upsertCredential, {411 authedMember.mutation(api.workspace.upsertCredential, {
416 workspaceId: asWorkspaceId(owner.workspaceId),412 workspaceId: owner.workspaceId,
417 sourceKey: "openapi:github",413 sourceKey: "openapi:github",
418 scope: "workspace",414 scope: "workspace",
419 secretJson: { token: "ghp_test" },415 secretJson: { token: "ghp_test" },
@@ -441,7 +437,7 @@ describe("workspace access controls", () => { · 21 unmodified lines
441 437
442 await expect(438 await expect(
443 authedMember.mutation(api.workspace.upsertToolSource, {439 authedMember.mutation(api.workspace.upsertToolSource, {
444 workspaceId: asWorkspaceId(owner.workspaceId),440 workspaceId: owner.workspaceId,
445 name: "my-tool",441 name: "my-tool",
446 type: "mcp",442 type: "mcp",
447 config: { url: "https://example.com" },443 config: { url: "https://example.com" },
@@ -469,7 +465,7 @@ describe("workspace access controls", () => { · 21 unmodified lines
469 465
470 await expect(466 await expect(
471 authedMember.mutation(api.workspace.deleteToolSource, {467 authedMember.mutation(api.workspace.deleteToolSource, {
472 workspaceId: asWorkspaceId(owner.workspaceId),468 workspaceId: owner.workspaceId,
473 sourceId: "src_nonexistent",469 sourceId: "src_nonexistent",
474 }),470 }),
475 ).rejects.toThrow("Only workspace admins can perform this action");471 ).rejects.toThrow("Only workspace admins can perform this action");
@@ -494,7 +490,7 @@ describe("workspace access controls", () => { · 18 unmodified lines
494 const authedMember = t.withIdentity({ subject: "read-policy-member" });490 const authedMember = t.withIdentity({ subject: "read-policy-member" });
495 491
496 const policies = await authedMember.query(api.workspace.listAccessPolicies, {492 const policies = await authedMember.query(api.workspace.listAccessPolicies, {
497 workspaceId: asWorkspaceId(owner.workspaceId),493 workspaceId: owner.workspaceId,
498 });494 });
499 495
500 expect(policies).toEqual([]);496 expect(policies).toEqual([]);
@@ -519,7 +515,7 @@ describe("workspace access controls", () => { · 18 unmodified lines
519 const authedMember = t.withIdentity({ subject: "read-ts-member" });515 const authedMember = t.withIdentity({ subject: "read-ts-member" });
520 516
521 const sources = await authedMember.query(api.workspace.listToolSources, {517 const sources = await authedMember.query(api.workspace.listToolSources, {
522 workspaceId: asWorkspaceId(owner.workspaceId),518 workspaceId: owner.workspaceId,
523 });519 });
524 520
525 expect(sources).toEqual([]);521 expect(sources).toEqual([]);
@@ -695,7 +691,7 @@ describe("cross-workspace isolation", () => { · 169 unmodified lines
695 // User A tries to read User B's workspace691 // User A tries to read User B's workspace
696 await expect(692 await expect(
697 authedA.query(api.workspace.listTasks, {693 authedA.query(api.workspace.listTasks, {
698 workspaceId: asWorkspaceId(userB.workspaceId),694 workspaceId: userB.workspaceId,
699 }),695 }),
700 ).rejects.toThrow("You are not a member of this workspace");696 ).rejects.toThrow("You are not a member of this workspace");
701 });697 });
@@ -709,7 +705,7 @@ describe("cross-workspace isolation", () => { · 7 unmodified lines
709 705
710 await expect(706 await expect(
711 authedA.mutation(api.executor.createTask, {707 authedA.mutation(api.executor.createTask, {
712 workspaceId: asWorkspaceId(userB.workspaceId),708 workspaceId: userB.workspaceId,
713 code: "console.log('pwned')",709 code: "console.log('pwned')",
714 }),710 }),
715 ).rejects.toThrow("You are not a member of this workspace");711 ).rejects.toThrow("You are not a member of this workspace");
@@ -724,7 +720,7 @@ describe("cross-workspace isolation", () => { · 8 unmodified lines
724 720
725 await expect(721 await expect(
726 authedA.mutation(api.workspace.upsertAccessPolicy, {722 authedA.mutation(api.workspace.upsertAccessPolicy, {
727 workspaceId: asWorkspaceId(userB.workspaceId),723 workspaceId: userB.workspaceId,
728 toolPathPattern: "*",724 toolPathPattern: "*",
729 decision: "allow",725 decision: "allow",
730 }),726 }),
@@ -738,7 +734,7 @@ describe("cross-workspace isolation", () => { · 7 unmodified lines
738 734
739 await expect(735 await expect(
740 authed.query(api.workspace.listTasks, {736 authed.query(api.workspace.listTasks, {
741 workspaceId: "invalid_workspace_id",737 workspaceId: "invalid_workspace_id" as Id<"workspaces">,
742 }),738 }),
743 ).rejects.toThrow();739 ).rejects.toThrow();
744 });740 });
@@ -1018,12 +1014,12 @@ describe("task creation with access controls", () => { · 273 unmodified lines
1018 const authed = t.withIdentity({ subject: "task-creator" });1014 const authed = t.withIdentity({ subject: "task-creator" });
1019 1015
1020 const result = await authed.mutation(api.executor.createTask, {1016 const result = await authed.mutation(api.executor.createTask, {
1021 workspaceId: asWorkspaceId(user.workspaceId),1017 workspaceId: user.workspaceId,
1022 code: "console.log('hello')",1018 code: "console.log('hello')",
1023 });1019 });
1024 1020
1025 expect(result.task.status).toBe("queued");1021 expect(result.task.status).toBe("queued");
1026 expect(result.task.workspaceId).toBe(asWorkspaceId(user.workspaceId));1022 expect(result.task.workspaceId).toBe(user.workspaceId);
1027 });1023 });
1028 1024
1029 test("task is scoped to workspace and visible to members", async () => {1025 test("task is scoped to workspace and visible to members", async () => {
@@ -1045,14 +1041,14 @@ describe("task creation with access controls", () => { · 15 unmodified lines
1045 // Owner creates a task1041 // Owner creates a task
1046 const authedOwner = t.withIdentity({ subject: "task-scope-owner" });1042 const authedOwner = t.withIdentity({ subject: "task-scope-owner" });
1047 await authedOwner.mutation(api.executor.createTask, {1043 await authedOwner.mutation(api.executor.createTask, {
1048 workspaceId: asWorkspaceId(owner.workspaceId),1044 workspaceId: owner.workspaceId,
1049 code: "1 + 1",1045 code: "1 + 1",
1050 });1046 });
1051 1047
1052 // Member can see it1048 // Member can see it
1053 const authedMember = t.withIdentity({ subject: "task-scope-member" });1049 const authedMember = t.withIdentity({ subject: "task-scope-member" });
1054 const tasks = await authedMember.query(api.workspace.listTasks, {1050 const tasks = await authedMember.query(api.workspace.listTasks, {
1055 workspaceId: asWorkspaceId(owner.workspaceId),1051 workspaceId: owner.workspaceId,
1056 });1052 });
1057 1053
1058 expect(tasks.length).toBe(1);1054 expect(tasks.length).toBe(1);
@@ -1067,7 +1063,7 @@ describe("task creation with access controls", () => { · 8 unmodified lines
1067 // Owner creates a task1063 // Owner creates a task
1068 const authedOwner = t.withIdentity({ subject: "task-vis-owner" });1064 const authedOwner = t.withIdentity({ subject: "task-vis-owner" });
1069 await authedOwner.mutation(api.executor.createTask, {1065 await authedOwner.mutation(api.executor.createTask, {
1070 workspaceId: asWorkspaceId(owner.workspaceId),1066 workspaceId: owner.workspaceId,
1071 code: "secret code",1067 code: "secret code",
1072 });1068 });
1073 1069
@@ -1076,7 +1072,7 @@ describe("task creation with access controls", () => { · 2 unmodified lines
1076 1072
1077 await expect(1073 await expect(
1078 authedOutsider.query(api.workspace.listTasks, {1074 authedOutsider.query(api.workspace.listTasks, {
1079 workspaceId: asWorkspaceId(owner.workspaceId),1075 workspaceId: owner.workspaceId,
1080 }),1076 }),
1081 ).rejects.toThrow("You are not a member of this workspace");1077 ).rejects.toThrow("You are not a member of this workspace");
1082 });1078 });
@@ -1090,7 +1086,7 @@ describe("credential security", () => { · 7 unmodified lines
1090 1086
1091 // Create a credential via admin1087 // Create a credential via admin
1092 await authed.mutation(api.workspace.upsertCredential, {1088 await authed.mutation(api.workspace.upsertCredential, {
1093 workspaceId: asWorkspaceId(owner.workspaceId),1089 workspaceId: owner.workspaceId,
1094 sourceKey: "openapi:stripe",1090 sourceKey: "openapi:stripe",
1095 scope: "workspace",1091 scope: "workspace",
1096 secretJson: { token: "sk_live_super_secret" },1092 secretJson: { token: "sk_live_super_secret" },
@@ -1098,7 +1094,7 @@ describe("credential security", () => { · 1 unmodified line
1098 1094
1099 // List should redact1095 // List should redact
1100 const credentials = await authed.query(api.workspace.listCredentials, {1096 const credentials = await authed.query(api.workspace.listCredentials, {
1101 workspaceId: asWorkspaceId(owner.workspaceId),1097 workspaceId: owner.workspaceId,
1102 });1098 });
1103 1099
1104 expect(credentials.length).toBe(1);1100 expect(credentials.length).toBe(1);
@@ -1126,7 +1122,7 @@ describe("credential security", () => { · 21 unmodified lines
1126 1122
1127 await expect(1123 await expect(
1128 authedMember.query(api.workspace.resolveCredential, {1124 authedMember.query(api.workspace.resolveCredential, {
1129 workspaceId: asWorkspaceId(owner.workspaceId),1125 workspaceId: owner.workspaceId,
1130 sourceKey: "openapi:github",1126 sourceKey: "openapi:github",
1131 scope: "workspace",1127 scope: "workspace",
1132 }),1128 }),
@@ -1155,7 +1151,7 @@ describe("role hierarchy validation", () => { · 22 unmodified lines
1155 1151
1156 // Admin can upsert access policies1152 // Admin can upsert access policies
1157 const policy = await authed.mutation(api.workspace.upsertAccessPolicy, {1153 const policy = await authed.mutation(api.workspace.upsertAccessPolicy, {
1158 workspaceId: asWorkspaceId(org.workspaceId),1154 workspaceId: org.workspaceId,
1159 toolPathPattern: "admin.*",1155 toolPathPattern: "admin.*",
1160 decision: "deny",1156 decision: "deny",
1161 });1157 });
@@ -1163,7 +1159,7 @@ describe("role hierarchy validation", () => { · 1 unmodified line
1163 1159
1164 // Admin can upsert tool sources1160 // Admin can upsert tool sources
1165 const source = await authed.mutation(api.workspace.upsertToolSource, {1161 const source = await authed.mutation(api.workspace.upsertToolSource, {
1166 workspaceId: asWorkspaceId(org.workspaceId),1162 workspaceId: org.workspaceId,
1167 name: "admin-tool",1163 name: "admin-tool",
1168 type: "mcp",1164 type: "mcp",
1169 config: { url: "https://example.com" },1165 config: { url: "https://example.com" },
executor/convex/accounts.ts+7−9
@@ -11,11 +11,9 @@ async function deleteWorkspaceData( · 10 unmodified lines
11 return false;11 return false;
12 }12 }
13 13
14 const workspaceKey = String(workspaceId);
15
16 const taskDocs = await ctx.db14 const taskDocs = await ctx.db
17 .query("tasks")15 .query("tasks")
18 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceKey))16 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId))
19 .collect();17 .collect();
20 for (const task of taskDocs) {18 for (const task of taskDocs) {
21 const taskEvents = await ctx.db19 const taskEvents = await ctx.db
@@ -30,7 +28,7 @@ async function deleteWorkspaceData( · 8 unmodified lines
30 28
31 const approvalDocs = await ctx.db29 const approvalDocs = await ctx.db
32 .query("approvals")30 .query("approvals")
33 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceKey))31 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId))
34 .collect();32 .collect();
35 for (const approval of approvalDocs) {33 for (const approval of approvalDocs) {
36 await ctx.db.delete(approval._id);34 await ctx.db.delete(approval._id);
@@ -38,7 +36,7 @@ async function deleteWorkspaceData( · 1 unmodified line
38 36
39 const policyDocs = await ctx.db37 const policyDocs = await ctx.db
40 .query("accessPolicies")38 .query("accessPolicies")
41 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceKey))39 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId))
42 .collect();40 .collect();
43 for (const policy of policyDocs) {41 for (const policy of policyDocs) {
44 await ctx.db.delete(policy._id);42 await ctx.db.delete(policy._id);
@@ -46,7 +44,7 @@ async function deleteWorkspaceData( · 1 unmodified line
46 44
47 const credentialDocs = await ctx.db45 const credentialDocs = await ctx.db
48 .query("sourceCredentials")46 .query("sourceCredentials")
49 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceKey))47 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId))
50 .collect();48 .collect();
51 for (const credential of credentialDocs) {49 for (const credential of credentialDocs) {
52 await ctx.db.delete(credential._id);50 await ctx.db.delete(credential._id);
@@ -54,7 +52,7 @@ async function deleteWorkspaceData( · 1 unmodified line
54 52
55 const toolSources = await ctx.db53 const toolSources = await ctx.db
56 .query("toolSources")54 .query("toolSources")
57 .withIndex("by_workspace_updated", (q) => q.eq("workspaceId", workspaceKey))55 .withIndex("by_workspace_updated", (q) => q.eq("workspaceId", workspaceId))
58 .collect();56 .collect();
59 for (const source of toolSources) {57 for (const source of toolSources) {
60 await ctx.db.delete(source._id);58 await ctx.db.delete(source._id);
@@ -62,7 +60,7 @@ async function deleteWorkspaceData( · 1 unmodified line
62 60
63 const agentTasks = await ctx.db61 const agentTasks = await ctx.db
64 .query("agentTasks")62 .query("agentTasks")
65 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceKey))63 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId))
66 .collect();64 .collect();
67 for (const agentTask of agentTasks) {65 for (const agentTask of agentTasks) {
68 await ctx.db.delete(agentTask._id);66 await ctx.db.delete(agentTask._id);
@@ -70,7 +68,7 @@ async function deleteWorkspaceData( · 1 unmodified line
70 68
71 const cachedToolsets = await ctx.db69 const cachedToolsets = await ctx.db
72 .query("workspaceToolCache")70 .query("workspaceToolCache")
73 .withIndex("by_workspace", (q) => q.eq("workspaceId", workspaceKey))71 .withIndex("by_workspace", (q) => q.eq("workspaceId", workspaceId))
74 .collect();72 .collect();
75 for (const cacheEntry of cachedToolsets) {73 for (const cacheEntry of cachedToolsets) {
76 await ctx.storage.delete(cacheEntry.storageId).catch(() => {});74 await ctx.storage.delete(cacheEntry.storageId).catch(() => {});
executor/convex/credentialsNode.ts+1−1
@@ -141,7 +141,7 @@ async function upsertVaultObject(args: { · 140 unmodified lines
141export const upsertCredential = action({141export const upsertCredential = action({
142 args: {142 args: {
143 id: v.optional(v.string()),143 id: v.optional(v.string()),
144 workspaceId: v.string(),144 workspaceId: v.id("workspaces"),
145 sessionId: v.optional(v.string()),145 sessionId: v.optional(v.string()),
146 sourceKey: v.string(),146 sourceKey: v.string(),
147 scope: credentialScopeValidator,147 scope: credentialScopeValidator,
executor/convex/database.test.ts+35−9
@@ -1,6 +1,7 @@
1import { expect, test } from "bun:test";1import { expect, test } from "bun:test";
2import { convexTest } from "convex-test";2import { convexTest } from "convex-test";
3import { internal } from "./_generated/api";3import { internal } from "./_generated/api";
4import type { Id } from "./_generated/dataModel";
4import schema from "./schema";5import schema from "./schema";
5 6
6function setup() {7function setup() {
@@ -10,14 +11,36 @@ function setup() { · 3 unmodified lines
10 });11 });
11}12}
12 13
14/** Insert a minimal workspace row and return its Id. */
15async function seedWorkspace(t: ReturnType<typeof setup>, name = "test-ws"): Promise<Id<"workspaces">> {
16 return await t.run(async (ctx) => {
17 const orgId = await ctx.db.insert("organizations", {
18 name: `${name}-org`,
19 slug: `${name}-org`,
20 status: "active",
21 createdAt: Date.now(),
22 updatedAt: Date.now(),
23 });
24 return await ctx.db.insert("workspaces", {
25 name,
26 slug: name,
27 organizationId: orgId,
28 plan: "free",
29 createdAt: Date.now(),
30 updatedAt: Date.now(),
31 });
32 });
33}
34
13test("task lifecycle supports queue, run, and complete", async () => {35test("task lifecycle supports queue, run, and complete", async () => {
14 const t = setup();36 const t = setup();
37 const wsId = await seedWorkspace(t, "ws_1");
15 38
16 const created = await t.mutation(internal.database.createTask, {39 const created = await t.mutation(internal.database.createTask, {
17 id: "task_1",40 id: "task_1",
18 code: "console.log('hello')",41 code: "console.log('hello')",
19 runtimeId: "local-bun",42 runtimeId: "local-bun",
20 workspaceId: "ws_1",43 workspaceId: wsId,
21 actorId: "actor_1",44 actorId: "actor_1",
22 clientId: "web",45 clientId: "web",
23 });46 });
@@ -49,12 +72,13 @@ test("task lifecycle supports queue, run, and complete", async () => { · 25 unmodified lines
49 72
50test("approval lifecycle tracks pending and resolution", async () => {73test("approval lifecycle tracks pending and resolution", async () => {
51 const t = setup();74 const t = setup();
75 const wsId = await seedWorkspace(t, "ws_2");
52 76
53 await t.mutation(internal.database.createTask, {77 await t.mutation(internal.database.createTask, {
54 id: "task_2",78 id: "task_2",
55 code: "await tools.admin.delete_data({ id: 'x' })",79 code: "await tools.admin.delete_data({ id: 'x' })",
56 runtimeId: "local-bun",80 runtimeId: "local-bun",
57 workspaceId: "ws_2",81 workspaceId: wsId,
58 actorId: "actor_2",82 actorId: "actor_2",
59 clientId: "web",83 clientId: "web",
60 });84 });
@@ -67,7 +91,7 @@ test("approval lifecycle tracks pending and resolution", async () => { · 6 unmodified lines
67 });91 });
68 expect(createdApproval.status).toBe("pending");92 expect(createdApproval.status).toBe("pending");
69 93
70 const pending = await t.query(internal.database.listPendingApprovals, { workspaceId: "ws_2" });94 const pending = await t.query(internal.database.listPendingApprovals, { workspaceId: wsId });
71 expect(pending.length).toBe(1);95 expect(pending.length).toBe(1);
72 expect(pending[0]?.task.id).toBe("task_2");96 expect(pending[0]?.task.id).toBe("task_2");
73 97
@@ -78,7 +102,7 @@ test("approval lifecycle tracks pending and resolution", async () => { · 4 unmodified lines
78 });102 });
79 expect(resolved?.status).toBe("approved");103 expect(resolved?.status).toBe("approved");
80 104
81 const pendingAfter = await t.query(internal.database.listPendingApprovals, { workspaceId: "ws_2" });105 const pendingAfter = await t.query(internal.database.listPendingApprovals, { workspaceId: wsId });
82 expect(pendingAfter).toEqual([]);106 expect(pendingAfter).toEqual([]);
83});107});
84 108
@@ -140,9 +164,10 @@ test("bootstrap honors MCP caller-provided session id", async () => { · 55 unmodified lines
140 164
141test("credentials persist provider and resolve by scope", async () => {165test("credentials persist provider and resolve by scope", async () => {
142 const t = setup();166 const t = setup();
167 const wsId = await seedWorkspace(t, "ws_cred");
143 168
144 const workspaceCredential = await t.mutation(internal.database.upsertCredential, {169 const workspaceCredential = await t.mutation(internal.database.upsertCredential, {
145 workspaceId: "ws_cred",170 workspaceId: wsId,
146 sourceKey: "openapi:github",171 sourceKey: "openapi:github",
147 scope: "workspace",172 scope: "workspace",
148 provider: "managed",173 provider: "managed",
@@ -152,7 +177,7 @@ test("credentials persist provider and resolve by scope", async () => { · 3 unmodified lines
152 expect(workspaceCredential.provider).toBe("managed");177 expect(workspaceCredential.provider).toBe("managed");
153 178
154 const actorCredential = await t.mutation(internal.database.upsertCredential, {179 const actorCredential = await t.mutation(internal.database.upsertCredential, {
155 workspaceId: "ws_cred",180 workspaceId: wsId,
156 sourceKey: "openapi:github",181 sourceKey: "openapi:github",
157 scope: "actor",182 scope: "actor",
158 actorId: "actor_cred",183 actorId: "actor_cred",
@@ -164,14 +189,14 @@ test("credentials persist provider and resolve by scope", async () => { · 5 unmodified lines
164 expect(actorCredential.actorId).toBe("actor_cred");189 expect(actorCredential.actorId).toBe("actor_cred");
165 190
166 const resolvedWorkspace = await t.query(internal.database.resolveCredential, {191 const resolvedWorkspace = await t.query(internal.database.resolveCredential, {
167 workspaceId: "ws_cred",192 workspaceId: wsId,
168 sourceKey: "openapi:github",193 sourceKey: "openapi:github",
169 scope: "workspace",194 scope: "workspace",
170 });195 });
171 expect(resolvedWorkspace?.provider).toBe("managed");196 expect(resolvedWorkspace?.provider).toBe("managed");
172 197
173 const resolvedActor = await t.query(internal.database.resolveCredential, {198 const resolvedActor = await t.query(internal.database.resolveCredential, {
174 workspaceId: "ws_cred",199 workspaceId: wsId,
175 sourceKey: "openapi:github",200 sourceKey: "openapi:github",
176 scope: "actor",201 scope: "actor",
177 actorId: "actor_cred",202 actorId: "actor_cred",
@@ -181,9 +206,10 @@ test("credentials persist provider and resolve by scope", async () => { · 3 unmodified lines
181 206
182test("upsertCredential defaults provider to managed", async () => {207test("upsertCredential defaults provider to managed", async () => {
183 const t = setup();208 const t = setup();
209 const wsId = await seedWorkspace(t, "ws_default_provider");
184 210
185 const credential = await t.mutation(internal.database.upsertCredential, {211 const credential = await t.mutation(internal.database.upsertCredential, {
186 workspaceId: "ws_default_provider",212 workspaceId: wsId,
187 sourceKey: "openapi:stripe",213 sourceKey: "openapi:stripe",
188 scope: "workspace",214 scope: "workspace",
189 secretJson: { token: "sk_test_123" },215 secretJson: { token: "sk_test_123" },
executor/convex/database.ts+16−16
@@ -5,7 +5,7 @@ query, · 4 unmodified lines
5type MutationCtx, type QueryCtx } from "./_generated/server";5type MutationCtx, type QueryCtx } from "./_generated/server";
6import { internalMutation, internalQuery } from "./_generated/server";6import { internalMutation, internalQuery } from "./_generated/server";
7import { ensureUniqueSlug } from "../lib/slug";7import { ensureUniqueSlug } from "../lib/slug";
8import type { TaskStatus } from "@executor/contracts";8import type { TaskStatus } from "../lib/types";
9 9
10const DEFAULT_TIMEOUT_MS = 300_000;10const DEFAULT_TIMEOUT_MS = 300_000;
11type OrganizationRole = "owner" | "admin" | "member" | "billing_admin";11type OrganizationRole = "owner" | "admin" | "member" | "billing_admin";
@@ -335,7 +335,7 @@ export const createTask = internalMutation({ · 323 unmodified lines
335 runtimeId: v.string(),335 runtimeId: v.string(),
336 timeoutMs: v.optional(v.number()),336 timeoutMs: v.optional(v.number()),
337 metadata: v.optional(v.any()),337 metadata: v.optional(v.any()),
338 workspaceId: v.string(),338 workspaceId: v.id("workspaces"),
339 actorId: v.string(),339 actorId: v.string(),
340 clientId: v.optional(v.string()),340 clientId: v.optional(v.string()),
341 },341 },
@@ -377,7 +377,7 @@ export const getTask = internalQuery({ · 35 unmodified lines
377});377});
378 378
379export const listTasks = internalQuery({379export const listTasks = internalQuery({
380 args: { workspaceId: v.string() },380 args: { workspaceId: v.id("workspaces") },
381 handler: async (ctx, args) => {381 handler: async (ctx, args) => {
382 const docs = await ctx.db382 const docs = await ctx.db
383 .query("tasks")383 .query("tasks")
@@ -415,7 +415,7 @@ export const listRuntimeTargets = internalQuery({ · 31 unmodified lines
415});415});
416 416
417export const getTaskInWorkspace = internalQuery({417export const getTaskInWorkspace = internalQuery({
418 args: { taskId: v.string(), workspaceId: v.string() },418 args: { taskId: v.string(), workspaceId: v.id("workspaces") },
419 handler: async (ctx, args) => {419 handler: async (ctx, args) => {
420 const doc = await getTaskDoc(ctx, args.taskId);420 const doc = await getTaskDoc(ctx, args.taskId);
421 if (!doc || doc.workspaceId !== args.workspaceId) {421 if (!doc || doc.workspaceId !== args.workspaceId) {
@@ -523,7 +523,7 @@ export const getApproval = internalQuery({ · 101 unmodified lines
523 523
524export const listApprovals = internalQuery({524export const listApprovals = internalQuery({
525 args: {525 args: {
526 workspaceId: v.string(),526 workspaceId: v.id("workspaces"),
527 status: v.optional(approvalStatusValidator),527 status: v.optional(approvalStatusValidator),
528 },528 },
529 handler: async (ctx, args) => {529 handler: async (ctx, args) => {
@@ -549,7 +549,7 @@ export const listApprovals = internalQuery({ · 19 unmodified lines
549});549});
550 550
551export const listPendingApprovals = internalQuery({551export const listPendingApprovals = internalQuery({
552 args: { workspaceId: v.string() },552 args: { workspaceId: v.id("workspaces") },
553 handler: async (ctx, args) => {553 handler: async (ctx, args) => {
554 const docs = await ctx.db554 const docs = await ctx.db
555 .query("approvals")555 .query("approvals")
@@ -616,7 +616,7 @@ export const resolveApproval = internalMutation({ · 60 unmodified lines
616});616});
617 617
618export const getApprovalInWorkspace = internalQuery({618export const getApprovalInWorkspace = internalQuery({
619 args: { approvalId: v.string(), workspaceId: v.string() },619 args: { approvalId: v.string(), workspaceId: v.id("workspaces") },
620 handler: async (ctx, args) => {620 handler: async (ctx, args) => {
621 const doc = await getApprovalDoc(ctx, args.approvalId);621 const doc = await getApprovalDoc(ctx, args.approvalId);
622 if (!doc || doc.workspaceId !== args.workspaceId) {622 if (!doc || doc.workspaceId !== args.workspaceId) {
@@ -656,7 +656,7 @@ export const createAgentTask = mutation({ · 33 unmodified lines
656 id: v.string(),656 id: v.string(),
657 prompt: v.string(),657 prompt: v.string(),
658 requesterId: v.string(),658 requesterId: v.string(),
659 workspaceId: v.string(),659 workspaceId: v.id("workspaces"),
660 actorId: v.string(),660 actorId: v.string(),
661 },661 },
662 handler: async (ctx, args) => {662 handler: async (ctx, args) => {
@@ -796,7 +796,7 @@ export const bootstrapAnonymousSession = internalMutation({ · 133 unmodified lines
796export const upsertAccessPolicy = internalMutation({796export const upsertAccessPolicy = internalMutation({
797 args: {797 args: {
798 id: v.optional(v.string()),798 id: v.optional(v.string()),
799 workspaceId: v.string(),799 workspaceId: v.id("workspaces"),
800 actorId: v.optional(v.string()),800 actorId: v.optional(v.string()),
801 clientId: v.optional(v.string()),801 clientId: v.optional(v.string()),
802 toolPathPattern: v.string(),802 toolPathPattern: v.string(),
@@ -847,7 +847,7 @@ export const upsertAccessPolicy = internalMutation({ · 44 unmodified lines
847});847});
848 848
849export const listAccessPolicies = internalQuery({849export const listAccessPolicies = internalQuery({
850 args: { workspaceId: v.string() },850 args: { workspaceId: v.id("workspaces") },
851 handler: async (ctx, args) => {851 handler: async (ctx, args) => {
852 const docs = await ctx.db852 const docs = await ctx.db
853 .query("accessPolicies")853 .query("accessPolicies")
@@ -868,7 +868,7 @@ export const listAccessPolicies = internalQuery({ · 14 unmodified lines
868export const upsertCredential = internalMutation({868export const upsertCredential = internalMutation({
869 args: {869 args: {
870 id: v.optional(v.string()),870 id: v.optional(v.string()),
871 workspaceId: v.string(),871 workspaceId: v.id("workspaces"),
872 sourceKey: v.string(),872 sourceKey: v.string(),
873 scope: credentialScopeValidator,873 scope: credentialScopeValidator,
874 actorId: v.optional(v.string()),874 actorId: v.optional(v.string()),
@@ -932,7 +932,7 @@ export const upsertCredential = internalMutation({ · 57 unmodified lines
932});932});
933 933
934export const listCredentials = internalQuery({934export const listCredentials = internalQuery({
935 args: { workspaceId: v.string() },935 args: { workspaceId: v.id("workspaces") },
936 handler: async (ctx, args) => {936 handler: async (ctx, args) => {
937 const docs = await ctx.db937 const docs = await ctx.db
938 .query("sourceCredentials")938 .query("sourceCredentials")
@@ -963,7 +963,7 @@ export const listCredentialProviders = internalQuery({ · 24 unmodified lines
963 963
964export const resolveCredential = internalQuery({964export const resolveCredential = internalQuery({
965 args: {965 args: {
966 workspaceId: v.string(),966 workspaceId: v.id("workspaces"),
967 sourceKey: v.string(),967 sourceKey: v.string(),
968 scope: credentialScopeValidator,968 scope: credentialScopeValidator,
969 actorId: v.optional(v.string()),969 actorId: v.optional(v.string()),
@@ -1007,7 +1007,7 @@ export const resolveCredential = internalQuery({ · 37 unmodified lines
1007export const upsertToolSource = internalMutation({1007export const upsertToolSource = internalMutation({
1008 args: {1008 args: {
1009 id: v.optional(v.string()),1009 id: v.optional(v.string()),
1010 workspaceId: v.string(),1010 workspaceId: v.id("workspaces"),
1011 name: v.string(),1011 name: v.string(),
1012 type: toolSourceTypeValidator,1012 type: toolSourceTypeValidator,
1013 config: v.any(),1013 config: v.any(),
@@ -1065,7 +1065,7 @@ export const upsertToolSource = internalMutation({ · 51 unmodified lines
1065});1065});
1066 1066
1067export const listToolSources = internalQuery({1067export const listToolSources = internalQuery({
1068 args: { workspaceId: v.string() },1068 args: { workspaceId: v.id("workspaces") },
1069 handler: async (ctx, args) => {1069 handler: async (ctx, args) => {
1070 const docs = await ctx.db1070 const docs = await ctx.db
1071 .query("toolSources")1071 .query("toolSources")
@@ -1077,7 +1077,7 @@ export const listToolSources = internalQuery({ · 5 unmodified lines
1077});1077});
1078 1078
1079export const deleteToolSource = internalMutation({1079export const deleteToolSource = internalMutation({
1080 args: { workspaceId: v.string(), sourceId: v.string() },1080 args: { workspaceId: v.id("workspaces"), sourceId: v.string() },
1081 handler: async (ctx, args) => {1081 handler: async (ctx, args) => {
1082 const doc = await ctx.db1082 const doc = await ctx.db
1083 .query("toolSources")1083 .query("toolSources")
executor/convex/executor-mcp.e2e.test.ts+3−2
@@ -4,6 +4,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; · 3 unmodified lines
4import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";4import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";5import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6import { internal } from "./_generated/api";6import { internal } from "./_generated/api";
7import type { Id } from "./_generated/dataModel";
7import schema from "./schema";8import schema from "./schema";
8 9
9function setup() {10function setup() {
@@ -46,7 +47,7 @@ function createMcpTransport( · 36 unmodified lines
46async function waitForTaskId(t: ReturnType<typeof setup>, workspaceId: string, timeoutMs = 10_000): Promise<string> {47async function waitForTaskId(t: ReturnType<typeof setup>, workspaceId: string, timeoutMs = 10_000): Promise<string> {
47 const start = Date.now();48 const start = Date.now();
48 while (Date.now() - start < timeoutMs) {49 while (Date.now() - start < timeoutMs) {
49 const tasks = await t.query(internal.database.listTasks, { workspaceId });50 const tasks = await t.query(internal.database.listTasks, { workspaceId: workspaceId as Id<"workspaces"> });
50 if (tasks.length > 0) {51 if (tasks.length > 0) {
51 return tasks[0]!.id;52 return tasks[0]!.id;
52 }53 }
@@ -64,7 +65,7 @@ async function waitForPendingApproval( · 11 unmodified lines
64): Promise<string> {65): Promise<string> {
65 const start = Date.now();66 const start = Date.now();
66 while (Date.now() - start < timeoutMs) {67 while (Date.now() - start < timeoutMs) {
67 const approvals = await t.query(internal.database.listPendingApprovals, { workspaceId });68 const approvals = await t.query(internal.database.listPendingApprovals, { workspaceId: workspaceId as Id<"workspaces"> });
68 const approval = approvals.find((item: { toolPath: string; id: string }) => item.toolPath === toolPath);69 const approval = approvals.find((item: { toolPath: string; id: string }) => item.toolPath === toolPath);
69 if (approval) {70 if (approval) {
70 return approval.id;71 return approval.id;
executor/convex/executor.ts+5−4
@@ -1,5 +1,6 @@
1import { v } from "convex/values";1import { v } from "convex/values";
2import { internal } from "./_generated/api";2import { internal } from "./_generated/api";
3import type { Id } from "./_generated/dataModel";
3import type { MutationCtx } from "./_generated/server";4import type { MutationCtx } from "./_generated/server";
4import { internalMutation } from "./_generated/server";5import { internalMutation } from "./_generated/server";
5import { workspaceMutation } from "../lib/functionBuilders";6import { workspaceMutation } from "../lib/functionBuilders";
@@ -30,7 +31,7 @@ async function createTaskRecord( · 24 unmodified lines
30 timeoutMs?: number;31 timeoutMs?: number;
31 runtimeId?: string;32 runtimeId?: string;
32 metadata?: unknown;33 metadata?: unknown;
33 workspaceId: string;34 workspaceId: Id<"workspaces">;
34 actorId: string;35 actorId: string;
35 clientId?: string;36 clientId?: string;
36 },37 },
@@ -92,7 +93,7 @@ async function createTaskRecord( · 55 unmodified lines
92async function resolveApprovalRecord(93async function resolveApprovalRecord(
93 ctx: MutationCtx,94 ctx: MutationCtx,
94 args: {95 args: {
95 workspaceId: string;96 workspaceId: Id<"workspaces">;
96 approvalId: string;97 approvalId: string;
97 decision: "approved" | "denied";98 decision: "approved" | "denied";
98 reviewerId?: string;99 reviewerId?: string;
@@ -172,7 +173,7 @@ export const createTaskInternal = internalMutation({ · 73 unmodified lines
172 timeoutMs: v.optional(v.number()),173 timeoutMs: v.optional(v.number()),
173 runtimeId: v.optional(v.string()),174 runtimeId: v.optional(v.string()),
174 metadata: v.optional(v.any()),175 metadata: v.optional(v.any()),
175 workspaceId: v.string(),176 workspaceId: v.id("workspaces"),
176 actorId: v.string(),177 actorId: v.string(),
177 clientId: v.optional(v.string()),178 clientId: v.optional(v.string()),
178 },179 },
@@ -204,7 +205,7 @@ export const resolveApproval = workspaceMutation({ · 25 unmodified lines
204 205
205export const resolveApprovalInternal = internalMutation({206export const resolveApprovalInternal = internalMutation({
206 args: {207 args: {
207 workspaceId: v.string(),208 workspaceId: v.id("workspaces"),
208 approvalId: v.string(),209 approvalId: v.string(),
209 decision: v.union(v.literal("approved"), v.literal("denied")),210 decision: v.union(v.literal("approved"), v.literal("denied")),
210 reviewerId: v.optional(v.string()),211 reviewerId: v.optional(v.string()),
executor/convex/executorNode.ts+4−6
@@ -525,8 +525,6 @@ function toToolDescriptor(tool: ToolDefinition, approval: "auto" | "required"): · 524 unmodified lines
525 operationId: tool.metadata?.operationId,525 operationId: tool.metadata?.operationId,
526 // Note: sourceDts is NOT included — it's too large to send over the wire.526 // Note: sourceDts is NOT included — it's too large to send over the wire.
527 // Monaco fetches .d.ts blobs separately via Convex storage URLs.527 // Monaco fetches .d.ts blobs separately via Convex storage URLs.
528 // Legacy compat
529 schemaTypes: tool.metadata?.schemaTypes,
530 };528 };
531}529}
532 530
@@ -910,7 +908,7 @@ async function invokeTool(ctx: any, task: TaskRecord, call: ToolCallRequest): Pr · 377 unmodified lines
910 908
911export const listTools = action({909export const listTools = action({
912 args: {910 args: {
913 workspaceId: v.string(),911 workspaceId: v.id("workspaces"),
914 actorId: v.optional(v.string()),912 actorId: v.optional(v.string()),
915 clientId: v.optional(v.string()),913 clientId: v.optional(v.string()),
916 sessionId: v.optional(v.string()),914 sessionId: v.optional(v.string()),
@@ -939,7 +937,7 @@ export const listTools = action({ · 22 unmodified lines
939 937
940export const listToolsWithWarnings = action({938export const listToolsWithWarnings = action({
941 args: {939 args: {
942 workspaceId: v.string(),940 workspaceId: v.id("workspaces"),
943 actorId: v.optional(v.string()),941 actorId: v.optional(v.string()),
944 clientId: v.optional(v.string()),942 clientId: v.optional(v.string()),
945 sessionId: v.optional(v.string()),943 sessionId: v.optional(v.string()),
@@ -976,7 +974,7 @@ export const listToolsWithWarnings = action({ · 30 unmodified lines
976 974
977export const listToolsInternal = internalAction({975export const listToolsInternal = internalAction({
978 args: {976 args: {
979 workspaceId: v.string(),977 workspaceId: v.id("workspaces"),
980 actorId: v.optional(v.string()),978 actorId: v.optional(v.string()),
981 clientId: v.optional(v.string()),979 clientId: v.optional(v.string()),
982 },980 },
@@ -987,7 +985,7 @@ export const listToolsInternal = internalAction({ · 4 unmodified lines
987 985
988export const listToolsWithWarningsInternal = internalAction({986export const listToolsWithWarningsInternal = internalAction({
989 args: {987 args: {
990 workspaceId: v.string(),988 workspaceId: v.id("workspaces"),
991 actorId: v.optional(v.string()),989 actorId: v.optional(v.string()),
992 clientId: v.optional(v.string()),990 clientId: v.optional(v.string()),
993 },991 },
executor/convex/http.ts+15−8
@@ -6,6 +6,7 @@ import { httpAction } from "./_generated/server"; · 5 unmodified lines
6import { authKit } from "./auth";6import { authKit } from "./auth";
7import { handleMcpRequest, type McpWorkspaceContext } from "../lib/mcp_server";7import { handleMcpRequest, type McpWorkspaceContext } from "../lib/mcp_server";
8import type { AnonymousContext, PendingApprovalRecord, TaskRecord, ToolDescriptor } from "../lib/types";8import type { AnonymousContext, PendingApprovalRecord, TaskRecord, ToolDescriptor } from "../lib/types";
9import type { Id } from "./_generated/dataModel";
9 10
10const http = httpRouter();11const http = httpRouter();
11const internalToken = process.env.EXECUTOR_INTERNAL_TOKEN ?? null;12const internalToken = process.env.EXECUTOR_INTERNAL_TOKEN ?? null;
@@ -161,7 +162,7 @@ const mcpHandler = httpAction(async (ctx, request) => { · 149 unmodified lines
161 try {162 try {
162 if (mcpAuthConfig.enabled && auth?.subject) {163 if (mcpAuthConfig.enabled && auth?.subject) {
163 const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForWorkosSubject, {164 const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForWorkosSubject, {
164 workspaceId: requestedContext.workspaceId,165 workspaceId: requestedContext.workspaceId as Id<"workspaces">,
165 subject: auth.subject,166 subject: auth.subject,
166 });167 });
167 168
@@ -176,7 +177,7 @@ const mcpHandler = httpAction(async (ctx, request) => { · 8 unmodified lines
176 }177 }
177 178
178 const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForRequest, {179 const access = await ctx.runQuery(internal.workspaceAuthInternal.getWorkspaceAccessForRequest, {
179 workspaceId: requestedContext.workspaceId,180 workspaceId: requestedContext.workspaceId as Id<"workspaces">,
180 sessionId: requestedContext.sessionId,181 sessionId: requestedContext.sessionId,
181 });182 });
182 183
@@ -212,11 +213,14 @@ const mcpHandler = httpAction(async (ctx, request) => { · 29 unmodified lines
212 actorId: string;213 actorId: string;
213 clientId?: string;214 clientId?: string;
214 }) => {215 }) => {
215 return (await ctx.runMutation(internal.executor.createTaskInternal, input)) as { task: TaskRecord };216 return (await ctx.runMutation(internal.executor.createTaskInternal, {
217 ...input,
218 workspaceId: input.workspaceId as Id<"workspaces">,
219 })) as { task: TaskRecord };
216 },220 },
217 getTask: async (taskId: string, workspaceId?: string) => {221 getTask: async (taskId: string, workspaceId?: string) => {
218 if (workspaceId) {222 if (workspaceId) {
219 return (await ctx.runQuery(internal.database.getTaskInWorkspace, { taskId, workspaceId })) as TaskRecord | null;223 return (await ctx.runQuery(internal.database.getTaskInWorkspace, { taskId, workspaceId: workspaceId as Id<"workspaces"> })) as TaskRecord | null;
220 }224 }
221 return null;225 return null;
222 },226 },
@@ -231,10 +235,10 @@ const mcpHandler = httpAction(async (ctx, request) => { · 8 unmodified lines
231 return [];235 return [];
232 }236 }
233 237
234 return (await ctx.runAction(internal.executorNode.listToolsInternal, toolContext)) as ToolDescriptor[];238 return (await ctx.runAction(internal.executorNode.listToolsInternal, { ...toolContext, workspaceId: toolContext.workspaceId as Id<"workspaces"> })) as ToolDescriptor[];
235 },239 },
236 listToolsForTypecheck: async (toolContext: { workspaceId: string; actorId?: string; clientId?: string }) => {240 listToolsForTypecheck: async (toolContext: { workspaceId: string; actorId?: string; clientId?: string }) => {
237 const result = await ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, toolContext) as {241 const result = await ctx.runAction(internal.executorNode.listToolsWithWarningsInternal, { ...toolContext, workspaceId: toolContext.workspaceId as Id<"workspaces"> }) as {
238 tools: ToolDescriptor[];242 tools: ToolDescriptor[];
239 dtsUrls?: Record<string, string>;243 dtsUrls?: Record<string, string>;
240 };244 };
@@ -245,7 +249,7 @@ const mcpHandler = httpAction(async (ctx, request) => { · 4 unmodified lines
245 };249 };
246 },250 },
247 listPendingApprovals: async (workspaceId: string) => {251 listPendingApprovals: async (workspaceId: string) => {
248 return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId })) as PendingApprovalRecord[];252 return (await ctx.runQuery(internal.database.listPendingApprovals, { workspaceId: workspaceId as Id<"workspaces"> })) as PendingApprovalRecord[];
249 },253 },
250 resolveApproval: async (input: {254 resolveApproval: async (input: {
251 workspaceId: string;255 workspaceId: string;
@@ -254,7 +258,10 @@ const mcpHandler = httpAction(async (ctx, request) => { · 2 unmodified lines
254 reviewerId?: string;258 reviewerId?: string;
255 reason?: string;259 reason?: string;
256 }) => {260 }) => {
257 return await ctx.runMutation(internal.executor.resolveApprovalInternal, input);261 return await ctx.runMutation(internal.executor.resolveApprovalInternal, {
262 ...input,
263 workspaceId: input.workspaceId as Id<"workspaces">,
264 });
258 },265 },
259 };266 };
260 267
executor/convex/migrations.ts+20−0
@@ -92,4 +92,24 @@ export const deleteInvitesMissingProvider = migrations.define({ · 91 unmodified lines
92 },92 },
93});93});
94 94
95export const deleteAnonymousSessionsMissingUserId = migrations.define({
96 table: "anonymousSessions",
97 migrateOne: async (ctx, session) => {
98 if (session.userId) {
99 return;
100 }
101
102 await ctx.db.delete(session._id);
103 },
104});
105
106export const backfillDtsStorageIds = migrations.define({
107 table: "workspaceToolCache",
108 migrateOne: async (_ctx, entry) => {
109 if (entry.dtsStorageIds === undefined) {
110 return { dtsStorageIds: [] };
111 }
112 },
113});
114
95export const run = migrations.runner();115export const run = migrations.runner();
executor/convex/package.json+7−0
@@ -0,0 +1,7 @@
1{
2 "name": "@executor/convex",
3 "version": "0.0.1",
4 "private": true,
5 "main": "./_generated/api.js",
6 "types": "./_generated/api.d.ts"
7}
executor/convex/schema.ts+10−25
@@ -181,7 +181,7 @@ export default defineSchema({ · 180 unmodified lines
181 taskId: v.string(),181 taskId: v.string(),
182 code: v.string(),182 code: v.string(),
183 runtimeId: v.string(),183 runtimeId: v.string(),
184 workspaceId: v.string(),184 workspaceId: v.id("workspaces"),
185 actorId: v.string(),185 actorId: v.string(),
186 clientId: v.string(),186 clientId: v.string(),
187 status: taskStatus,187 status: taskStatus,
@@ -203,7 +203,7 @@ export default defineSchema({ · 15 unmodified lines
203 approvals: defineTable({203 approvals: defineTable({
204 approvalId: v.string(),204 approvalId: v.string(),
205 taskId: v.string(),205 taskId: v.string(),
206 workspaceId: v.string(),206 workspaceId: v.id("workspaces"),
207 toolPath: v.string(),207 toolPath: v.string(),
208 input: v.any(),208 input: v.any(),
209 status: approvalStatus,209 status: approvalStatus,
@@ -217,21 +217,6 @@ export default defineSchema({ · 7 unmodified lines
217 .index("by_workspace_created", ["workspaceId", "createdAt"])217 .index("by_workspace_created", ["workspaceId", "createdAt"])
218 .index("by_workspace_status_created", ["workspaceId", "status", "createdAt"]),218 .index("by_workspace_status_created", ["workspaceId", "status", "createdAt"]),
219 219
220 pushSubscriptions: defineTable({
221 workspaceId: v.string(),
222 accountId: v.string(),
223 endpoint: v.string(),
224 p256dh: v.string(),
225 auth: v.string(),
226 expirationTime: v.optional(v.number()),
227 userAgent: v.optional(v.string()),
228 createdAt: v.number(),
229 updatedAt: v.number(),
230 })
231 .index("by_workspace_created", ["workspaceId", "createdAt"])
232 .index("by_workspace_account", ["workspaceId", "accountId"])
233 .index("by_endpoint", ["endpoint"]),
234
235 taskEvents: defineTable({220 taskEvents: defineTable({
236 sequence: v.number(),221 sequence: v.number(),
237 taskId: v.string(),222 taskId: v.string(),
@@ -245,7 +230,7 @@ export default defineSchema({ · 7 unmodified lines
245 230
246 accessPolicies: defineTable({231 accessPolicies: defineTable({
247 policyId: v.string(),232 policyId: v.string(),
248 workspaceId: v.string(),233 workspaceId: v.id("workspaces"),
249 actorId: v.string(),234 actorId: v.string(),
250 clientId: v.string(),235 clientId: v.string(),
251 toolPathPattern: v.string(),236 toolPathPattern: v.string(),
@@ -259,7 +244,7 @@ export default defineSchema({ · 7 unmodified lines
259 244
260 sourceCredentials: defineTable({245 sourceCredentials: defineTable({
261 credentialId: v.string(),246 credentialId: v.string(),
262 workspaceId: v.string(),247 workspaceId: v.id("workspaces"),
263 sourceKey: v.string(),248 sourceKey: v.string(),
264 scope: credentialScope,249 scope: credentialScope,
265 actorId: v.string(),250 actorId: v.string(),
@@ -274,7 +259,7 @@ export default defineSchema({ · 8 unmodified lines
274 259
275 toolSources: defineTable({260 toolSources: defineTable({
276 sourceId: v.string(),261 sourceId: v.string(),
277 workspaceId: v.string(),262 workspaceId: v.id("workspaces"),
278 name: v.string(),263 name: v.string(),
279 type: toolSourceType,264 type: toolSourceType,
280 config: v.any(),265 config: v.any(),
@@ -291,7 +276,7 @@ export default defineSchema({ · 10 unmodified lines
291 agentTaskId: v.string(),276 agentTaskId: v.string(),
292 prompt: v.string(),277 prompt: v.string(),
293 requesterId: v.string(),278 requesterId: v.string(),
294 workspaceId: v.string(),279 workspaceId: v.id("workspaces"),
295 actorId: v.string(),280 actorId: v.string(),
296 status: agentTaskStatus,281 status: agentTaskStatus,
297 resultText: v.optional(v.string()),282 resultText: v.optional(v.string()),
@@ -314,14 +299,14 @@ export default defineSchema({ · 16 unmodified lines
314 .index("by_spec_url_version", ["specUrl", "version"]),299 .index("by_spec_url_version", ["specUrl", "version"]),
315 300
316 workspaceToolCache: defineTable({301 workspaceToolCache: defineTable({
317 workspaceId: v.string(),302 workspaceId: v.id("workspaces"),
318 signature: v.string(),303 signature: v.string(),
319 storageId: v.id("_storage"),304 storageId: v.id("_storage"),
320 /** Per-source .d.ts blobs stored separately (too large for action responses). */305 /** Per-source .d.ts blobs stored separately (too large for action responses). */
321 dtsStorageIds: v.optional(v.array(v.object({306 dtsStorageIds: v.array(v.object({
322 sourceKey: v.string(),307 sourceKey: v.string(),
323 storageId: v.id("_storage"),308 storageId: v.id("_storage"),
324 }))),309 })),
325 toolCount: v.number(),310 toolCount: v.number(),
326 sizeBytes: v.number(),311 sizeBytes: v.number(),
327 createdAt: v.number(),312 createdAt: v.number(),
@@ -334,7 +319,7 @@ export default defineSchema({ · 6 unmodified lines
334 actorId: v.string(),319 actorId: v.string(),
335 clientId: v.string(),320 clientId: v.string(),
336 accountId: v.id("accounts"),321 accountId: v.id("accounts"),
337 userId: v.optional(v.id("workspaceMembers")),322 userId: v.id("workspaceMembers"),
338 createdAt: v.number(),323 createdAt: v.number(),
339 lastSeenAt: v.number(),324 lastSeenAt: v.number(),
340 })325 })
executor/convex/workspaceAuthInternal.ts+2−2
@@ -12,7 +12,7 @@ function actorIdForAccount(account: { _id: string; provider: string; providerAcc · 11 unmodified lines
12 12
13export const getWorkspaceAccessForRequest = internalQuery({13export const getWorkspaceAccessForRequest = internalQuery({
14 args: {14 args: {
15 workspaceId: v.string(),15 workspaceId: v.id("workspaces"),
16 sessionId: v.optional(v.string()),16 sessionId: v.optional(v.string()),
17 },17 },
18 handler: async (ctx, args) => {18 handler: async (ctx, args) => {
@@ -36,7 +36,7 @@ export const getWorkspaceAccessForRequest = internalQuery({ · 17 unmodified lines
36 36
37export const getWorkspaceAccessForWorkosSubject = internalQuery({37export const getWorkspaceAccessForWorkosSubject = internalQuery({
38 args: {38 args: {
39 workspaceId: v.string(),39 workspaceId: v.id("workspaces"),
40 subject: v.string(),40 subject: v.string(),
41 },41 },
42 handler: async (ctx, args) => {42 handler: async (ctx, args) => {
executor/convex/workspaceToolCache.ts+9−11
@@ -7,7 +7,7 @@ import { internalMutation, internalQuery } from "./_generated/server"; · 6 unmodified lines
7 */7 */
8export const getEntry = internalQuery({8export const getEntry = internalQuery({
9 args: {9 args: {
10 workspaceId: v.string(),10 workspaceId: v.id("workspaces"),
11 signature: v.string(),11 signature: v.string(),
12 },12 },
13 handler: async (ctx, args) => {13 handler: async (ctx, args) => {
@@ -21,7 +21,7 @@ export const getEntry = internalQuery({ · 7 unmodified lines
21 21
22 return {22 return {
23 storageId: entry.storageId,23 storageId: entry.storageId,
24 dtsStorageIds: entry.dtsStorageIds ?? [],24 dtsStorageIds: entry.dtsStorageIds,
25 toolCount: entry.toolCount,25 toolCount: entry.toolCount,
26 sizeBytes: entry.sizeBytes,26 sizeBytes: entry.sizeBytes,
27 createdAt: entry.createdAt,27 createdAt: entry.createdAt,
@@ -35,13 +35,13 @@ export const getEntry = internalQuery({ · 7 unmodified lines
35 */35 */
36export const putEntry = internalMutation({36export const putEntry = internalMutation({
37 args: {37 args: {
38 workspaceId: v.string(),38 workspaceId: v.id("workspaces"),
39 signature: v.string(),39 signature: v.string(),
40 storageId: v.id("_storage"),40 storageId: v.id("_storage"),
41 dtsStorageIds: v.optional(v.array(v.object({41 dtsStorageIds: v.array(v.object({
42 sourceKey: v.string(),42 sourceKey: v.string(),
43 storageId: v.id("_storage"),43 storageId: v.id("_storage"),
44 }))),44 })),
45 toolCount: v.number(),45 toolCount: v.number(),
46 sizeBytes: v.number(),46 sizeBytes: v.number(),
47 },47 },
@@ -55,10 +55,8 @@ export const putEntry = internalMutation({ · 7 unmodified lines
55 // Delete old main snapshot blob55 // Delete old main snapshot blob
56 await ctx.storage.delete(existing.storageId).catch(() => {});56 await ctx.storage.delete(existing.storageId).catch(() => {});
57 // Delete old .d.ts blobs57 // Delete old .d.ts blobs
58 if (existing.dtsStorageIds) {58 for (const entry of existing.dtsStorageIds) {
59 for (const entry of existing.dtsStorageIds) {59 await ctx.storage.delete(entry.storageId).catch(() => {});
60 await ctx.storage.delete(entry.storageId).catch(() => {});
61 }
62 }60 }
63 await ctx.db.delete(existing._id);61 await ctx.db.delete(existing._id);
64 }62 }
@@ -80,7 +78,7 @@ export const putEntry = internalMutation({ · 15 unmodified lines
80 */78 */
81export const getDtsStorageIds = internalQuery({79export const getDtsStorageIds = internalQuery({
82 args: {80 args: {
83 workspaceId: v.string(),81 workspaceId: v.id("workspaces"),
84 },82 },
85 handler: async (ctx, args) => {83 handler: async (ctx, args) => {
86 const entry = await ctx.db84 const entry = await ctx.db
@@ -89,6 +87,6 @@ export const getDtsStorageIds = internalQuery({ · 2 unmodified lines
89 .unique();87 .unique();
90 88
91 if (!entry) return [];89 if (!entry) return [];
92 return entry.dtsStorageIds ?? [];90 return entry.dtsStorageIds;
93 },91 },
94});92});
executor/convex/workspaces.ts+6−8
@@ -20,8 +20,6 @@ async function workspaceHasActivity( · 19 unmodified lines
20 ctx: Pick<QueryCtx, "db"> | Pick<MutationCtx, "db">,20 ctx: Pick<QueryCtx, "db"> | Pick<MutationCtx, "db">,
21 workspaceId: Id<"workspaces">,21 workspaceId: Id<"workspaces">,
22): Promise<boolean> {22): Promise<boolean> {
23 const workspaceIdString = String(workspaceId);
24
25 const [23 const [
26 task,24 task,
27 approval,25 approval,
@@ -31,15 +29,15 @@ async function workspaceHasActivity( · 3 unmodified lines
31 agentTask,29 agentTask,
32 member,30 member,
33 ] = await Promise.all([31 ] = await Promise.all([
34 ctx.db.query("tasks").withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceIdString)).first(),32 ctx.db.query("tasks").withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId)).first(),
35 ctx.db.query("approvals").withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceIdString)).first(),33 ctx.db.query("approvals").withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId)).first(),
36 ctx.db.query("accessPolicies").withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceIdString)).first(),34 ctx.db.query("accessPolicies").withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId)).first(),
37 ctx.db35 ctx.db
38 .query("sourceCredentials")36 .query("sourceCredentials")
39 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceIdString))37 .withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId))
40 .first(),38 .first(),
41 ctx.db.query("toolSources").withIndex("by_workspace_updated", (q) => q.eq("workspaceId", workspaceIdString)).first(),39 ctx.db.query("toolSources").withIndex("by_workspace_updated", (q) => q.eq("workspaceId", workspaceId)).first(),
42 ctx.db.query("agentTasks").withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceIdString)).first(),40 ctx.db.query("agentTasks").withIndex("by_workspace_created", (q) => q.eq("workspaceId", workspaceId)).first(),
43 ctx.db.query("workspaceMembers").withIndex("by_workspace", (q) => q.eq("workspaceId", workspaceId)).first(),41 ctx.db.query("workspaceMembers").withIndex("by_workspace", (q) => q.eq("workspaceId", workspaceId)).first(),
44 ]);42 ]);
45 43
executor/lib/credential-providers.test.ts+0−9
@@ -46,15 +46,6 @@ test("WorkOS Vault provider falls back to id key and raw token", async () => { · 45 unmodified lines
46 expect(payload).toEqual({ token: "ghp_raw_token" });46 expect(payload).toEqual({ token: "ghp_raw_token" });
47});47});
48 48
49test("WorkOS Vault provider supports legacy inline payload", async () => {
50 const payload = await resolveCredentialPayload({
51 provider: "workos-vault",
52 secretJson: { token: "legacy-token" },
53 });
54
55 expect(payload).toEqual({ token: "legacy-token" });
56});
57
58test("WorkOS Vault provider gives actionable error on missing reference", async () => {49test("WorkOS Vault provider gives actionable error on missing reference", async () => {
59 await expect(50 await expect(
60 resolveCredentialPayload({51 resolveCredentialPayload({
executor/lib/credential_providers.ts+1−9
@@ -124,14 +124,6 @@ async function resolveWorkosVault( · 123 unmodified lines
124 : "";124 : "";
125 125
126 if (!objectId) {126 if (!objectId) {
127 // Backward compatibility: older rows may have stored credential payload directly
128 // under the encrypted provider before object references were persisted.
129 const { objectId: _objectId, id: _id, apiKey: _apiKey, ...legacyPayload } = config;
130 const normalized = asRecord(legacyPayload);
131 if (Object.keys(normalized).length > 0) {
132 return normalized;
133 }
134
135 throw new Error(127 throw new Error(
136 "Encrypted credential is missing its secure reference. Re-save this credential in the dashboard.",128 "Encrypted credential is missing its secure reference. Re-save this credential in the dashboard.",
137 );129 );
@@ -151,7 +143,7 @@ export async function resolveCredentialPayload( · 13 unmodified lines
151 record: Pick<CredentialRecord, "provider" | "secretJson">,143 record: Pick<CredentialRecord, "provider" | "secretJson">,
152 options?: { readVaultObject?: VaultObjectReader },144 options?: { readVaultObject?: VaultObjectReader },
153): Promise<CredentialPayload | null> {145): Promise<CredentialPayload | null> {
154 const provider = (record.provider ?? "managed") as CredentialProvider;146 const provider = record.provider;
155 const resolver = providers[provider];147 const resolver = providers[provider];
156 if (!resolver) {148 if (!resolver) {
157 throw new Error(`Unsupported credential provider: ${provider}`);149 throw new Error(`Unsupported credential provider: ${provider}`);
executor/lib/functionBuilders.ts+2−2
@@ -138,7 +138,7 @@ export const organizationMutation = customMutation(mutation, { · 137 unmodified lines
138 138
139export const workspaceQuery = customQuery(query, {139export const workspaceQuery = customQuery(query, {
140 args: {140 args: {
141 workspaceId: v.string(),141 workspaceId: v.id("workspaces"),
142 sessionId: v.optional(v.string()),142 sessionId: v.optional(v.string()),
143 },143 },
144 input: async (ctx, args, options: WorkspaceAccessOptions = {}) => {144 input: async (ctx, args, options: WorkspaceAccessOptions = {}) => {
@@ -158,7 +158,7 @@ export const workspaceQuery = customQuery(query, { · 13 unmodified lines
158 158
159export const workspaceMutation = customMutation(mutation, {159export const workspaceMutation = customMutation(mutation, {
160 args: {160 args: {
161 workspaceId: v.string(),161 workspaceId: v.id("workspaces"),
162 sessionId: v.optional(v.string()),162 sessionId: v.optional(v.string()),
163 },163 },
164 input: async (ctx, args, options: WorkspaceAccessOptions = {}) => {164 input: async (ctx, args, options: WorkspaceAccessOptions = {}) => {
executor/lib/identity.ts+3−3
@@ -100,10 +100,10 @@ export async function getWorkspaceMembership( · 99 unmodified lines
100 100
101export async function requireWorkspaceAccessForAccount(101export async function requireWorkspaceAccessForAccount(
102 ctx: MembershipCtx,102 ctx: MembershipCtx,
103 workspaceId: string,103 workspaceId: Id<"workspaces">,
104 account: Doc<"accounts">,104 account: Doc<"accounts">,
105): Promise<WorkspaceAccess> {105): Promise<WorkspaceAccess> {
106 const workspace = await ctx.db.get(workspaceId as Id<"workspaces">);106 const workspace = await ctx.db.get(workspaceId);
107 if (!workspace) {107 if (!workspace) {
108 throw new Error("Workspace not found");108 throw new Error("Workspace not found");
109 }109 }
@@ -140,7 +140,7 @@ export async function requireWorkspaceAccessForAccount( · 30 unmodified lines
140 140
141export async function requireWorkspaceAccessForRequest(141export async function requireWorkspaceAccessForRequest(
142 ctx: IdentityCtx,142 ctx: IdentityCtx,
143 workspaceId: string,143 workspaceId: Id<"workspaces">,
144 sessionId?: string,144 sessionId?: string,
145): Promise<WorkspaceAccess> {145): Promise<WorkspaceAccess> {
146 const account = await resolveAccountForRequest(ctx, sessionId);146 const account = await resolveAccountForRequest(ctx, sessionId);
executor/lib/mcp-server.test.ts+2−0
@@ -78,6 +78,8 @@ class FakeMcpService { · 77 unmodified lines
78 workspaceId: `ws_${crypto.randomUUID()}`,78 workspaceId: `ws_${crypto.randomUUID()}`,
79 actorId: `anon_${crypto.randomUUID()}`,79 actorId: `anon_${crypto.randomUUID()}`,
80 clientId: "mcp",80 clientId: "mcp",
81 accountId: `account_${crypto.randomUUID()}`,
82 userId: `user_${crypto.randomUUID()}`,
81 createdAt: now,83 createdAt: now,
82 lastSeenAt: now,84 lastSeenAt: now,
83 };85 };
executor/lib/openapi-cache.test.ts+0−188
@@ -168,110 +168,7 @@ describe("prepareOpenApiSpec with large specs", () => { · 167 unmodified lines
168 expect(json.length).toBeGreaterThan(100_000);168 expect(json.length).toBeGreaterThan(100_000);
169 });169 });
170 170
171 test("preserves operationTypes when spec is large", async () => {
172 const spec = makeLargeSpec(50);
173 const prepared = await prepareOpenApiSpec(spec, "type-test");
174 171
175 // openapiTS should generate types for all operations
176 if (prepared.operationTypes) {
177 const opCount = Object.keys(prepared.operationTypes).length;
178 // Should have types for most operations (openapiTS generates from valid specs)
179 expect(opCount).toBeGreaterThan(0);
180
181 // Spot-check a known operation
182 const getType = prepared.operationTypes["get_resource_0"];
183 if (getType) {
184 expect(getType.argsType).toBeDefined();
185 expect(getType.returnsType).toBeDefined();
186 }
187 }
188 });
189
190 test("preserves schemaTypes when spec has shared schemas", async () => {
191 const specWithSchemas: Record<string, unknown> = {
192 openapi: "3.0.3",
193 info: { title: "Schema Test", version: "1.0.0" },
194 servers: [{ url: "https://api.example.com" }],
195 components: {
196 schemas: {
197 User: {
198 type: "object",
199 properties: {
200 id: { type: "string" },
201 email: { type: "string" },
202 name: { type: "string" },
203 role: { type: "string", enum: ["admin", "user", "viewer"] },
204 },
205 required: ["id", "email"],
206 },
207 UserList: {
208 type: "object",
209 properties: {
210 data: {
211 type: "array",
212 items: { $ref: "#/components/schemas/User" },
213 },
214 total: { type: "number" },
215 },
216 },
217 },
218 },
219 paths: {
220 "/users": {
221 get: {
222 operationId: "listUsers",
223 tags: ["users"],
224 summary: "List users",
225 responses: {
226 "200": {
227 description: "ok",
228 content: {
229 "application/json": {
230 schema: { $ref: "#/components/schemas/UserList" },
231 },
232 },
233 },
234 },
235 },
236 },
237 "/users/{id}": {
238 get: {
239 operationId: "getUser",
240 tags: ["users"],
241 parameters: [
242 {
243 name: "id",
244 in: "path",
245 required: true,
246 schema: { type: "string" },
247 },
248 ],
249 summary: "Get user",
250 responses: {
251 "200": {
252 description: "ok",
253 content: {
254 "application/json": {
255 schema: { $ref: "#/components/schemas/User" },
256 },
257 },
258 },
259 },
260 },
261 },
262 },
263 };
264
265 const prepared = await prepareOpenApiSpec(specWithSchemas, "schema-test");
266
267 // Should have generated schema types
268 if (prepared.schemaTypes) {
269 expect(Object.keys(prepared.schemaTypes).length).toBeGreaterThan(0);
270 }
271
272 // No warnings
273 expect(prepared.warnings).toHaveLength(0);
274 });
275});172});
276 173
277describe("buildOpenApiToolsFromPrepared", () => {174describe("buildOpenApiToolsFromPrepared", () => {
@@ -946,78 +843,7 @@ describe("buildOpenApiToolsFromPrepared", () => { · 668 unmodified lines
946 expect(tool!.metadata?.returnsType).not.toBe("unknown");843 expect(tool!.metadata?.returnsType).not.toBe("unknown");
947 });844 });
948 845
949 test("schemaTypes only attached to first tool from a source", async () => {
950 const specWithSchemas: Record<string, unknown> = {
951 openapi: "3.0.3",
952 info: { title: "Schema placement test", version: "1.0.0" },
953 servers: [{ url: "https://api.example.com" }],
954 components: {
955 schemas: {
956 Widget: {
957 type: "object",
958 properties: {
959 id: { type: "string" },
960 name: { type: "string" },
961 },
962 },
963 },
964 },
965 paths: {
966 "/widgets": {
967 get: {
968 operationId: "listWidgets",
969 tags: ["widgets"],
970 responses: {
971 "200": {
972 description: "ok",
973 content: {
974 "application/json": {
975 schema: {
976 type: "array",
977 items: { $ref: "#/components/schemas/Widget" },
978 },
979 },
980 },
981 },
982 },
983 },
984 post: {
985 operationId: "createWidget",
986 tags: ["widgets"],
987 requestBody: {
988 content: {
989 "application/json": {
990 schema: { $ref: "#/components/schemas/Widget" },
991 },
992 },
993 },
994 responses: {
995 "201": { description: "created" },
996 },
997 },
998 },
999 },
1000 };
1001 846
1002 const prepared = await prepareOpenApiSpec(specWithSchemas, "schema-placement");
1003 const tools = buildOpenApiToolsFromPrepared(
1004 {
1005 type: "openapi",
1006 name: "widgets",
1007 spec: specWithSchemas,
1008 baseUrl: "https://api.example.com",
1009 },
1010 prepared,
1011 );
1012
1013 expect(tools.length).toBe(2);
1014
1015 // Only the first tool should have schemaTypes
1016 const withSchemas = tools.filter(
1017 (t) => t.metadata?.schemaTypes && Object.keys(t.metadata.schemaTypes).length > 0,
1018 );
1019 expect(withSchemas.length).toBeLessThanOrEqual(1);
1020 });
1021});847});
1022 848
1023describe("prepared spec serialization round-trip", () => {849describe("prepared spec serialization round-trip", () => {
@@ -1035,20 +861,6 @@ describe("prepared spec serialization round-trip", () => { · 11 unmodified lines
1035 );861 );
1036 expect(restored.warnings).toEqual(prepared.warnings);862 expect(restored.warnings).toEqual(prepared.warnings);
1037 863
1038 if (prepared.operationTypes) {
1039 expect(restored.operationTypes).toBeDefined();
1040 expect(Object.keys(restored.operationTypes!).length).toBe(
1041 Object.keys(prepared.operationTypes).length,
1042 );
1043 }
1044
1045 if (prepared.schemaTypes) {
1046 expect(restored.schemaTypes).toBeDefined();
1047 expect(Object.keys(restored.schemaTypes!).length).toBe(
1048 Object.keys(prepared.schemaTypes).length,
1049 );
1050 }
1051
1052 // Verify tools built from the restored spec match the original864 // Verify tools built from the restored spec match the original
1053 const config = {865 const config = {
1054 type: "openapi" as const,866 type: "openapi" as const,
executor/lib/tool_sources.ts+1−28
@@ -852,10 +852,6 @@ export interface PreparedOpenApiSpec { · 851 unmodified lines
852 paths: Record<string, unknown>;852 paths: Record<string, unknown>;
853 /** Raw .d.ts from openapiTS — cached as-is, extracted lazily on read */853 /** Raw .d.ts from openapiTS — cached as-is, extracted lazily on read */
854 dts?: string;854 dts?: string;
855 /** @deprecated Kept for cache compat — new entries use `dts` instead */
856 operationTypes?: Record<string, { argsType: string; returnsType: string }>;
857 /** @deprecated Kept for cache compat — new entries use `dts` instead */
858 schemaTypes?: Record<string, string>;
859 warnings: string[];855 warnings: string[];
860}856}
861 857
@@ -941,7 +937,7 @@ export function buildOpenApiToolsFromPrepared( · 79 unmodified lines
941 const paths = asRecord(prepared.paths);937 const paths = asRecord(prepared.paths);
942 const tools: ToolDefinition[] = [];938 const tools: ToolDefinition[] = [];
943 939
944 // The raw .d.ts is attached to the first tool only (same pattern as old schemaTypes).940 // The raw .d.ts is attached to the first tool only (one per source to avoid duplication).
945 // The typechecker/Monaco use this directly via indexed access types.941 // The typechecker/Monaco use this directly via indexed access types.
946 const sourceDts = prepared.dts942 const sourceDts = prepared.dts
947 ? prepared.dts.replace(/^export /gm, "") // strip 'export' so types are ambient943 ? prepared.dts.replace(/^export /gm, "") // strip 'export' so types are ambient
@@ -1739,7 +1735,6 @@ export interface SerializedTool { · 791 unmodified lines
1739 * MCP: { kind: "mcp", url, transport?, queryParams?, toolName }1735 * MCP: { kind: "mcp", url, transport?, queryParams?, toolName }
1740 * GraphQL raw: { kind: "graphql_raw", endpoint, authHeaders }1736 * GraphQL raw: { kind: "graphql_raw", endpoint, authHeaders }
1741 * GraphQL field: { kind: "graphql_field", endpoint, operationName, operationType, queryTemplate, argNames?, authHeaders }1737 * GraphQL field: { kind: "graphql_field", endpoint, operationName, operationType, queryTemplate, argNames?, authHeaders }
1742 * GraphQL (legacy compat): { kind: "graphql", endpoint, operationName, operationType, authHeaders }
1743 * Builtin: { kind: "builtin" } — run comes from DEFAULT_TOOLS1738 * Builtin: { kind: "builtin" } — run comes from DEFAULT_TOOLS
1744 */1739 */
1745 runSpec:1740 runSpec:
@@ -1772,13 +1767,6 @@ export interface SerializedTool { · 26 unmodified lines
1772 argNames?: string[];1767 argNames?: string[];
1773 authHeaders: Record<string, string>;1768 authHeaders: Record<string, string>;
1774 }1769 }
1775 | {
1776 kind: "graphql";
1777 endpoint: string;
1778 operationName: string;
1779 operationType: "query" | "mutation";
1780 authHeaders: Record<string, string>;
1781 }
1782 | { kind: "builtin" };1770 | { kind: "builtin" };
1783}1771}
1784 1772
@@ -1976,21 +1964,6 @@ export function rehydrateTools( · 191 unmodified lines
1976 };1964 };
1977 }1965 }
1978 1966
1979 if (st.runSpec.kind === "graphql") {
1980 // Legacy compat for older cached entries.
1981 const { endpoint, operationName, operationType, authHeaders } = st.runSpec;
1982 const queryTemplate = `${operationType} ${operationName}($input: JSON) { ${operationName}(input: $input) }`;
1983 return {
1984 ...base,
1985 run: async (input: unknown, context) => {
1986 const payload = asRecord(input);
1987 const variables = payload.variables ?? { input: payload };
1988 const envelope = await executeGraphql(endpoint, authHeaders, queryTemplate, variables, context);
1989 return selectGraphqlFieldEnvelope(envelope, operationName);
1990 },
1991 };
1992 }
1993
1994 return { ...base, run: async () => { throw new Error(`Unknown run spec kind for '${st.path}'`); } };1967 return { ...base, run: async () => { throw new Error(`Unknown run spec kind for '${st.path}'`); } };
1995 });1968 });
1996}1969}
executor/lib/typechecker.test.ts+0−93
@@ -424,97 +424,4 @@ export interface operations { · 423 unmodified lines
424 });424 });
425});425});
426 426
427// ---------------------------------------------------------------------------
428// schemaTypes integration — verifies that schema type aliases are emitted and
429// correctly used by the typechecker
430// ---------------------------------------------------------------------------
431
432describe("typecheckCode with schemaTypes", () => {
433 const CUSTOMER_SCHEMA_TOOL: ToolDescriptor = {
434 path: "stripe.customers.create",
435 description: "Create a customer",
436 approval: "auto",
437 argsType: "{ name: string; email: string }",
438 returnsType: "Customer",
439 // Only the first tool from a source carries schemaTypes
440 schemaTypes: {
441 Customer: "{ id: string; name: string; email: string; subscriptions: Subscription[] }",
442 Subscription: "{ id: string; status: string; plan: Plan }",
443 Plan: "{ id: string; amount: number; currency: string }",
444 },
445 };
446
447 const CUSTOMER_GET_TOOL: ToolDescriptor = {
448 path: "stripe.customers.get",
449 description: "Get a customer",
450 approval: "auto",
451 argsType: "{ id: string }",
452 returnsType: "Customer",
453 // Subsequent tools do NOT carry schemaTypes (deduplication)
454 };
455
456 const STRIPE_TOOLS = [CUSTOMER_SCHEMA_TOOL, CUSTOMER_GET_TOOL];
457
458 test("schema types are emitted in declarations", () => {
459 const result = generateToolDeclarations(STRIPE_TOOLS);
460 expect(result).toContain("type Customer =");
461 expect(result).toContain("type Subscription =");
462 expect(result).toContain("type Plan =");
463 // Declarations should come before the tools declaration
464 const schemaIdx = result.indexOf("type Customer =");
465 const toolsIdx = result.indexOf("declare const tools:");
466 expect(schemaIdx).toBeLessThan(toolsIdx);
467 });
468
469 test("code using schema return types typechecks correctly", () => {
470 const decl = generateToolDeclarations(STRIPE_TOOLS);
471 const result = typecheckCode(
472 `const customer = await tools.stripe.customers.create({ name: "John", email: "john@test.com" });
473 const id: string = customer.id;
474 const subs: Subscription[] = customer.subscriptions;
475 const plan: Plan = subs[0].plan;
476 return plan.amount;`,
477 decl,
478 );
479 expect(result.ok).toBe(true);
480 expect(result.errors).toHaveLength(0);
481 });
482
483 test("wrong property access on schema type produces errors", () => {
484 const decl = generateToolDeclarations(STRIPE_TOOLS);
485 const result = typecheckCode(
486 `const customer = await tools.stripe.customers.get({ id: "cus_123" });
487 return customer.nonexistent_field;`,
488 decl,
489 );
490 expect(result.ok).toBe(false);
491 expect(result.errors.some((e) => e.includes("nonexistent_field"))).toBe(true);
492 });
493 427
494 test("transitive schema types are available (Subscription, Plan)", () => {
495 const decl = generateToolDeclarations(STRIPE_TOOLS);
496 const result = typecheckCode(
497 `const customer = await tools.stripe.customers.get({ id: "cus_123" });
498 const sub = customer.subscriptions[0];
499 const status: string = sub.status;
500 const amount: number = sub.plan.amount;
501 const currency: string = sub.plan.currency;
502 return { status, amount, currency };`,
503 decl,
504 );
505 expect(result.ok).toBe(true);
506 expect(result.errors).toHaveLength(0);
507 });
508
509 test("wrong type on schema property produces errors", () => {
510 const decl = generateToolDeclarations(STRIPE_TOOLS);
511 const result = typecheckCode(
512 `const customer = await tools.stripe.customers.get({ id: "cus_123" });
513 const amount: string = customer.subscriptions[0].plan.amount;`,
514 decl,
515 );
516 expect(result.ok).toBe(false);
517 // amount is number, assigning to string should fail
518 expect(result.errors.some((e) => e.includes("number") || e.includes("string"))).toBe(true);
519 });
520});
executor/lib/typechecker.ts+0−19
@@ -104,18 +104,6 @@ export function generateToolDeclarations( · 103 unmodified lines
104 options?: GenerateToolDeclarationOptions,104 options?: GenerateToolDeclarationOptions,
105): string {105): string {
106 106
107 // Legacy compat: collect schemaTypes from tools that use the old format
108 const allSchemas = new Map<string, string>();
109 for (const tool of tools) {
110 if (tool.schemaTypes) {
111 for (const [name, type] of Object.entries(tool.schemaTypes)) {
112 if (!allSchemas.has(name)) {
113 allSchemas.set(name, type);
114 }
115 }
116 }
117 }
118
119 // Build a nested tree from flat tool paths107 // Build a nested tree from flat tool paths
120 interface TreeNode {108 interface TreeNode {
121 children: Map<string, TreeNode>;109 children: Map<string, TreeNode>;
@@ -172,13 +160,6 @@ export function generateToolDeclarations( · 50 unmodified lines
172 // Assemble the full declarations block160 // Assemble the full declarations block
173 const parts: string[] = [];161 const parts: string[] = [];
174 162
175 // Legacy schema type aliases (from old cache format)
176 if (allSchemas.size > 0) {
177 for (const [name, type] of allSchemas) {
178 parts.push(`type ${name} = ${type};`);
179 }
180 }
181
182 const sourceDtsBySource = options?.sourceDtsBySource ?? {};163 const sourceDtsBySource = options?.sourceDtsBySource ?? {};
183 const dtsEntries = Object.entries(sourceDtsBySource)164 const dtsEntries = Object.entries(sourceDtsBySource)
184 .filter(([, dts]) => typeof dts === "string" && dts.trim().length > 0)165 .filter(([, dts]) => typeof dts === "string" && dts.trim().length > 0)
executor/lib/types.ts+122−24
@@ -1,25 +1,125 @@
1// Re-export shared types from contracts1// ── Shared types (inlined from @executor/contracts) ──────────────────────────
2export type {2
3 TaskStatus,3export type TaskStatus = "queued" | "running" | "completed" | "failed" | "timed_out" | "denied";
4 ApprovalStatus,4export type ApprovalStatus = "pending" | "approved" | "denied";
5 PolicyDecision,5export type PolicyDecision = "allow" | "require_approval" | "deny";
6 CredentialScope,6export type CredentialScope = "workspace" | "actor";
7 CredentialProvider,7export type CredentialProvider = "managed" | "workos-vault";
8 ToolApprovalMode,8export type ToolApprovalMode = "auto" | "required";
9 ToolSourceType,9export type ToolSourceType = "mcp" | "openapi" | "graphql";
10 TaskRecord,10
11 ApprovalRecord,11export interface TaskRecord {
12 PendingApprovalRecord,12 id: string;
13 TaskEventRecord,13 code: string;
14 AccessPolicyRecord,14 runtimeId: string;
15 CredentialRecord,15 status: TaskStatus;
16 ToolSourceRecord,16 timeoutMs: number;
17 ToolDescriptor,17 metadata: Record<string, unknown>;
18 OpenApiSourceQuality,18 workspaceId: string;
19 AnonymousContext,19 actorId?: string;
20} from "@executor/contracts";20 clientId?: string;
21 21 createdAt: number;
22import type { TaskStatus, CredentialScope, ToolApprovalMode } from "@executor/contracts";22 updatedAt: number;
23 startedAt?: number;
24 completedAt?: number;
25 error?: string;
26 stdout?: string;
27 stderr?: string;
28 exitCode?: number;
29}
30
31export interface ApprovalRecord {
32 id: string;
33 taskId: string;
34 toolPath: string;
35 input: unknown;
36 status: ApprovalStatus;
37 reason?: string;
38 reviewerId?: string;
39 createdAt: number;
40 resolvedAt?: number;
41}
42
43export interface PendingApprovalRecord extends ApprovalRecord {
44 task: Pick<TaskRecord, "id" | "status" | "runtimeId" | "timeoutMs" | "createdAt">;
45}
46
47export interface TaskEventRecord {
48 id: number;
49 taskId: string;
50 eventName: string;
51 type: string;
52 payload: unknown;
53 createdAt: number;
54}
55
56export interface AccessPolicyRecord {
57 id: string;
58 workspaceId: string;
59 actorId?: string;
60 clientId?: string;
61 toolPathPattern: string;
62 decision: PolicyDecision;
63 priority: number;
64 createdAt: number;
65 updatedAt: number;
66}
67
68export interface CredentialRecord {
69 id: string;
70 workspaceId: string;
71 sourceKey: string;
72 scope: CredentialScope;
73 actorId?: string;
74 provider: CredentialProvider;
75 secretJson: Record<string, unknown>;
76 createdAt: number;
77 updatedAt: number;
78}
79
80export interface ToolSourceRecord {
81 id: string;
82 workspaceId: string;
83 name: string;
84 type: ToolSourceType;
85 config: Record<string, unknown>;
86 enabled: boolean;
87 createdAt: number;
88 updatedAt: number;
89}
90
91export interface ToolDescriptor {
92 path: string;
93 description: string;
94 approval: ToolApprovalMode;
95 source?: string;
96 argsType?: string;
97 returnsType?: string;
98 operationId?: string;
99}
100
101export interface OpenApiSourceQuality {
102 sourceKey: string;
103 toolCount: number;
104 unknownArgsCount: number;
105 unknownReturnsCount: number;
106 partialUnknownArgsCount: number;
107 partialUnknownReturnsCount: number;
108 argsQuality: number;
109 returnsQuality: number;
110 overallQuality: number;
111}
112
113export interface AnonymousContext {
114 sessionId: string;
115 workspaceId: string;
116 actorId: string;
117 clientId: string;
118 accountId: string;
119 userId: string;
120 createdAt: number;
121 lastSeenAt: number;
122}
23 123
24// ── Server-only types ─────────────────────────────────────────────────────────124// ── Server-only types ─────────────────────────────────────────────────────────
25 125
@@ -120,8 +220,6 @@ export interface ToolTypeMetadata { · 94 unmodified lines
120 * Only set on the FIRST tool per source to avoid duplication.220 * Only set on the FIRST tool per source to avoid duplication.
121 */221 */
122 sourceDts?: string;222 sourceDts?: string;
123 /** @deprecated Use sourceDts instead. Kept for cache compat. */
124 schemaTypes?: Record<string, string>;
125}223}
126 224
127export interface ToolDefinition {225export interface ToolDefinition {
executor/lib/workspace-tool-cache.test.ts+37−17
@@ -5,6 +5,7 @@ · 4 unmodified lines
5 * round-trip with functional `run` methods.5 * round-trip with functional `run` methods.
6 */6 */
7import { test, expect, describe } from "bun:test";7import { test, expect, describe } from "bun:test";
8import type { Id } from "../convex/_generated/dataModel";
8import {9import {
9 prepareOpenApiSpec,10 prepareOpenApiSpec,
10 buildOpenApiToolsFromPrepared,11 buildOpenApiToolsFromPrepared,
@@ -464,8 +465,7 @@ describe("serializeTools + rehydrateTools round-trip", () => { · 453 unmodified lines
464});465});
465 466
466describe("workspace tool cache table", () => {467describe("workspace tool cache table", () => {
467 // convex-test based tests for the table operations468 async function setupCacheTest() {
468 test("getEntry + putEntry round-trip", async () => {
469 const { convexTest } = await import("convex-test");469 const { convexTest } = await import("convex-test");
470 const { internal } = await import("../convex/_generated/api");470 const { internal } = await import("../convex/_generated/api");
471 const schema = (await import("../convex/schema")).default;471 const schema = (await import("../convex/schema")).default;
@@ -475,9 +475,33 @@ describe("workspace tool cache table", () => { · 3 unmodified lines
475 "./_generated/api.js": () => import("../convex/_generated/api.js"),475 "./_generated/api.js": () => import("../convex/_generated/api.js"),
476 });476 });
477 477
478 const wsId = await t.run(async (ctx) => {
479 const orgId = await ctx.db.insert("organizations", {
480 name: "test-org",
481 slug: "test-org",
482 status: "active" as const,
483 createdAt: Date.now(),
484 updatedAt: Date.now(),
485 });
486 return await ctx.db.insert("workspaces", {
487 name: "test-ws",
488 slug: "test-ws",
489 organizationId: orgId,
490 plan: "free",
491 createdAt: Date.now(),
492 updatedAt: Date.now(),
493 });
494 });
495
496 return { t, internal, wsId };
497 }
498
499 test("getEntry + putEntry round-trip", async () => {
500 const { t, internal, wsId } = await setupCacheTest();
501
478 // Empty cache502 // Empty cache
479 const miss = await t.query(internal.workspaceToolCache.getEntry, {503 const miss = await t.query(internal.workspaceToolCache.getEntry, {
480 workspaceId: "ws_1",504 workspaceId: wsId,
481 signature: "sig_1",505 signature: "sig_1",
482 });506 });
483 expect(miss).toBeNull();507 expect(miss).toBeNull();
@@ -489,16 +513,17 @@ describe("workspace tool cache table", () => { · 5 unmodified lines
489 });513 });
490 514
491 await t.mutation(internal.workspaceToolCache.putEntry, {515 await t.mutation(internal.workspaceToolCache.putEntry, {
492 workspaceId: "ws_1",516 workspaceId: wsId,
493 signature: "sig_1",517 signature: "sig_1",
494 storageId,518 storageId,
495 toolCount: 0,519 toolCount: 0,
496 sizeBytes: 27,520 sizeBytes: 27,
521 dtsStorageIds: [],
497 });522 });
498 523
499 // Hit524 // Hit
500 const hit = await t.query(internal.workspaceToolCache.getEntry, {525 const hit = await t.query(internal.workspaceToolCache.getEntry, {
501 workspaceId: "ws_1",526 workspaceId: wsId,
502 signature: "sig_1",527 signature: "sig_1",
503 });528 });
504 expect(hit).not.toBeNull();529 expect(hit).not.toBeNull();
@@ -506,32 +531,26 @@ describe("workspace tool cache table", () => { · 1 unmodified line
506 531
507 // Wrong signature = miss532 // Wrong signature = miss
508 const wrongSig = await t.query(internal.workspaceToolCache.getEntry, {533 const wrongSig = await t.query(internal.workspaceToolCache.getEntry, {
509 workspaceId: "ws_1",534 workspaceId: wsId,
510 signature: "sig_2",535 signature: "sig_2",
511 });536 });
512 expect(wrongSig).toBeNull();537 expect(wrongSig).toBeNull();
513 });538 });
514 539
515 test("putEntry replaces old entry and deletes old blob", async () => {540 test("putEntry replaces old entry and deletes old blob", async () => {
516 const { convexTest } = await import("convex-test");541 const { t, internal, wsId } = await setupCacheTest();
517 const { internal } = await import("../convex/_generated/api");
518 const schema = (await import("../convex/schema")).default;
519
520 const t = convexTest(schema, {
521 "./workspaceToolCache.ts": () => import("../convex/workspaceToolCache"),
522 "./_generated/api.js": () => import("../convex/_generated/api.js"),
523 });
524 542
525 const storageId1 = await t.run(async (ctx) => {543 const storageId1 = await t.run(async (ctx) => {
526 return await ctx.storage.store(new Blob(["old"]));544 return await ctx.storage.store(new Blob(["old"]));
527 });545 });
528 546
529 await t.mutation(internal.workspaceToolCache.putEntry, {547 await t.mutation(internal.workspaceToolCache.putEntry, {
530 workspaceId: "ws_1",548 workspaceId: wsId,
531 signature: "sig_1",549 signature: "sig_1",
532 storageId: storageId1,550 storageId: storageId1,
533 toolCount: 5,551 toolCount: 5,
534 sizeBytes: 3,552 sizeBytes: 3,
553 dtsStorageIds: [],
535 });554 });
536 555
537 const storageId2 = await t.run(async (ctx) => {556 const storageId2 = await t.run(async (ctx) => {
@@ -539,16 +558,17 @@ describe("workspace tool cache table", () => { · 1 unmodified line
539 });558 });
540 559
541 await t.mutation(internal.workspaceToolCache.putEntry, {560 await t.mutation(internal.workspaceToolCache.putEntry, {
542 workspaceId: "ws_1",561 workspaceId: wsId,
543 signature: "sig_2",562 signature: "sig_2",
544 storageId: storageId2,563 storageId: storageId2,
545 toolCount: 10,564 toolCount: 10,
546 sizeBytes: 3,565 sizeBytes: 3,
566 dtsStorageIds: [],
547 });567 });
548 568
549 // New entry569 // New entry
550 const entry = await t.query(internal.workspaceToolCache.getEntry, {570 const entry = await t.query(internal.workspaceToolCache.getEntry, {
551 workspaceId: "ws_1",571 workspaceId: wsId,
552 signature: "sig_2",572 signature: "sig_2",
553 });573 });
554 expect(entry!.storageId).toBe(storageId2);574 expect(entry!.storageId).toBe(storageId2);
executor/mcp-gateway.ts+7−6
@@ -3,6 +3,7 @@ import { createRemoteJWKSet, jwtVerify } from "jose"; · 2 unmodified lines
3import { api } from "./convex/_generated/api";3import { api } from "./convex/_generated/api";
4import { handleMcpRequest, type McpWorkspaceContext } from "./lib/mcp_server";4import { handleMcpRequest, type McpWorkspaceContext } from "./lib/mcp_server";
5import type { AnonymousContext, PendingApprovalRecord, TaskRecord, ToolDescriptor } from "./lib/types";5import type { AnonymousContext, PendingApprovalRecord, TaskRecord, ToolDescriptor } from "./lib/types";
6import type { Id } from "./convex/_generated/dataModel";
6 7
7const convexUrlFromEnv = Bun.env.CONVEX_URL;8const convexUrlFromEnv = Bun.env.CONVEX_URL;
8if (!convexUrlFromEnv) {9if (!convexUrlFromEnv) {
@@ -126,7 +127,7 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 117 unmodified lines
126 clientId?: string;127 clientId?: string;
127 }) => {128 }) => {
128 const created = await convex.mutation(api.executor.createTask, {129 const created = await convex.mutation(api.executor.createTask, {
129 workspaceId: input.workspaceId,130 workspaceId: input.workspaceId as Id<"workspaces">,
130 sessionId,131 sessionId,
131 code: input.code,132 code: input.code,
132 timeoutMs: input.timeoutMs,133 timeoutMs: input.timeoutMs,
@@ -144,7 +145,7 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 11 unmodified lines
144 return null;145 return null;
145 }146 }
146 const task = await convex.query(api.workspace.getTaskInWorkspace, {147 const task = await convex.query(api.workspace.getTaskInWorkspace, {
147 workspaceId: effectiveWorkspaceId,148 workspaceId: effectiveWorkspaceId as Id<"workspaces">,
148 sessionId,149 sessionId,
149 taskId,150 taskId,
150 });151 });
@@ -165,7 +166,7 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 14 unmodified lines
165 return [];166 return [];
166 }167 }
167 const tools = await convex.action(api.executorNode.listTools, {168 const tools = await convex.action(api.executorNode.listTools, {
168 workspaceId: toolContext.workspaceId,169 workspaceId: toolContext.workspaceId as Id<"workspaces">,
169 sessionId,170 sessionId,
170 actorId: toolContext.actorId,171 actorId: toolContext.actorId,
171 clientId: toolContext.clientId,172 clientId: toolContext.clientId,
@@ -175,7 +176,7 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 3 unmodified lines
175 176
176 listPendingApprovals: async (approvalWorkspaceId: string) => {177 listPendingApprovals: async (approvalWorkspaceId: string) => {
177 const approvals = await convex.query(api.workspace.listPendingApprovals, {178 const approvals = await convex.query(api.workspace.listPendingApprovals, {
178 workspaceId: approvalWorkspaceId,179 workspaceId: approvalWorkspaceId as Id<"workspaces">,
179 sessionId,180 sessionId,
180 });181 });
181 return approvals as PendingApprovalRecord[];182 return approvals as PendingApprovalRecord[];
@@ -189,7 +190,7 @@ function createService(context?: McpWorkspaceContext, bearerToken?: string) { · 7 unmodified lines
189 reason?: string;190 reason?: string;
190 }) => {191 }) => {
191 return await convex.mutation(api.executor.resolveApproval, {192 return await convex.mutation(api.executor.resolveApproval, {
192 workspaceId: input.workspaceId,193 workspaceId: input.workspaceId as Id<"workspaces">,
193 sessionId,194 sessionId,
194 approvalId: input.approvalId,195 approvalId: input.approvalId,
195 decision: input.decision,196 decision: input.decision,
@@ -232,7 +233,7 @@ async function resolveContext( · 36 unmodified lines
232 }233 }
233 234
234 const requestContext = await convex.query(api.workspace.getRequestContext, {235 const requestContext = await convex.query(api.workspace.getRequestContext, {
235 workspaceId: requested.workspaceId,236 workspaceId: requested.workspaceId as Id<"workspaces">,
236 sessionId: requested.sessionId,237 sessionId: requested.sessionId,
237 });238 });
238 239
executor/packages/contracts/package.json+0−7
@@ -1,7 +0,0 @@
1{
2 "name": "@executor/contracts",
3 "version": "0.0.1",
4 "private": true,
5 "main": "src/index.ts",
6 "types": "src/index.ts"
7}
executor/packages/contracts/src/index.ts+0−143
@@ -1,143 +0,0 @@
1// ── Status enums ──────────────────────────────────────────────────────────────
2
3export type TaskStatus =
4 | "queued"
5 | "running"
6 | "completed"
7 | "failed"
8 | "timed_out"
9 | "denied";
10
11export type ApprovalStatus = "pending" | "approved" | "denied";
12
13export type PolicyDecision = "allow" | "require_approval" | "deny";
14
15export type CredentialScope = "workspace" | "actor";
16
17export type CredentialProvider = "managed" | "workos-vault";
18
19export type ToolApprovalMode = "auto" | "required";
20
21export type ToolSourceType = "mcp" | "openapi" | "graphql";
22
23// ── Records ───────────────────────────────────────────────────────────────────
24
25export interface TaskRecord {
26 id: string;
27 code: string;
28 runtimeId: string;
29 status: TaskStatus;
30 timeoutMs: number;
31 metadata: Record<string, unknown>;
32 workspaceId: string;
33 actorId?: string;
34 clientId?: string;
35 createdAt: number;
36 updatedAt: number;
37 startedAt?: number;
38 completedAt?: number;
39 error?: string;
40 stdout?: string;
41 stderr?: string;
42 exitCode?: number;
43}
44
45export interface ApprovalRecord {
46 id: string;
47 taskId: string;
48 toolPath: string;
49 input: unknown;
50 status: ApprovalStatus;
51 reason?: string;
52 reviewerId?: string;
53 createdAt: number;
54 resolvedAt?: number;
55}
56
57export interface PendingApprovalRecord extends ApprovalRecord {
58 task: Pick<TaskRecord, "id" | "status" | "runtimeId" | "timeoutMs" | "createdAt">;
59}
60
61export interface TaskEventRecord {
62 id: number;
63 taskId: string;
64 eventName: string;
65 type: string;
66 payload: unknown;
67 createdAt: number;
68}
69
70export interface AccessPolicyRecord {
71 id: string;
72 workspaceId: string;
73 actorId?: string;
74 clientId?: string;
75 toolPathPattern: string;
76 decision: PolicyDecision;
77 priority: number;
78 createdAt: number;
79 updatedAt: number;
80}
81
82export interface CredentialRecord {
83 id: string;
84 workspaceId: string;
85 sourceKey: string;
86 scope: CredentialScope;
87 actorId?: string;
88 provider: CredentialProvider;
89 secretJson: Record<string, unknown>;
90 createdAt: number;
91 updatedAt: number;
92}
93
94export interface ToolSourceRecord {
95 id: string;
96 workspaceId: string;
97 name: string;
98 type: ToolSourceType;
99 config: Record<string, unknown>;
100 enabled: boolean;
101 createdAt: number;
102 updatedAt: number;
103}
104
105// ── Descriptors ───────────────────────────────────────────────────────────────
106
107export interface ToolDescriptor {
108 path: string;
109 description: string;
110 approval: ToolApprovalMode;
111 source?: string;
112 /** Lightweight TS type hint for args (for LLM prompt / discover tool). */
113 argsType?: string;
114 /** Lightweight TS type hint for return value (for LLM prompt / discover tool). */
115 returnsType?: string;
116 /** Raw operationId from the OpenAPI spec (used by Monaco for indexed access types). */
117 operationId?: string;
118 /** @deprecated Kept for cache compat. */
119 schemaTypes?: Record<string, string>;
120}
121
122export interface OpenApiSourceQuality {
123 sourceKey: string;
124 toolCount: number;
125 unknownArgsCount: number;
126 unknownReturnsCount: number;
127 partialUnknownArgsCount: number;
128 partialUnknownReturnsCount: number;
129 argsQuality: number;
130 returnsQuality: number;
131 overallQuality: number;
132}
133
134export interface AnonymousContext {
135 sessionId: string;
136 workspaceId: string;
137 actorId: string;
138 clientId: string;
139 accountId?: string;
140 userId?: string;
141 createdAt: number;
142 lastSeenAt: number;
143}
executor/packages/contracts/tsconfig.json+0−11
@@ -1,11 +0,0 @@
1{
2 "compilerOptions": {
3 "strict": true,
4 "moduleResolution": "bundler",
5 "target": "ESNext",
6 "module": "ESNext",
7 "noEmit": true,
8 "skipLibCheck": true
9 },
10 "include": ["src/**/*.ts"]
11}
package.json+1−1
@@ -3,6 +3,7 @@ · 2 unmodified lines
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "workspaces": [5 "workspaces": [
6 "executor/convex",
6 "executor/apps/*",7 "executor/apps/*",
7 "executor/packages/*",8 "executor/packages/*",
8 "assistant",9 "assistant",
@@ -32,7 +33,6 @@ · 23 unmodified lines
32 "@convex-dev/migrations": "^0.3.1",33 "@convex-dev/migrations": "^0.3.1",
33 "@convex-dev/stripe": "^0.1.3",34 "@convex-dev/stripe": "^0.1.3",
34 "@convex-dev/workos-authkit": "^0.1.6",35 "@convex-dev/workos-authkit": "^0.1.6",
35 "@executor/contracts": "workspace:*",
36 "@workos-inc/node": "^8.2.0",36 "@workos-inc/node": "^8.2.0",
37 "convex": "^1.31.7",37 "convex": "^1.31.7",
38 "convex-helpers": "^0.1.111",38 "convex-helpers": "^0.1.111",

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t