History

Update environment configuration and remove deprecated web pages

- Added `EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY` variable to `.env.example` for runtime restrictions. - Updated `README.md` to document the new environment variable. - Removed obsolete web pages related to dashboard, approvals, billing, members, onboarding, organization, static app shell, tasks, and tools to streamline the codebase.

Rhys Sullivan committed 9 months ago

Commit ee0853e1 parent d407eb744 files · +631 −534Browse files

Unified · Split

44 files
  1. .env.example +4 −0
  2. executor/README.md +1 −0
  3. executor/apps/web/src/app/[[...dashboard]]/page.tsx +1 −1
  4. executor/apps/web/src/app/billing/page.tsx +0 −5
  5. executor/apps/web/src/app/members/page.tsx +0 −5
  6. executor/apps/web/src/app/onboarding/page.tsx +0 −5
  7. executor/apps/web/src/app/organization/page.tsx +0 −5
  8. executor/apps/web/src/app/page.tsx +0 −5
  9. executor/apps/web/src/app/static-app-shell/page.tsx +0 −5
  10. executor/apps/web/src/app/tasks/page.tsx +0 −5
  11. executor/apps/web/src/app/tools/page.tsx +0 −5
  12. executor/apps/web/src/components/tasks/task-composer.tsx +20 −15
  13. executor/apps/web/src/components/tools/explorer-tool-detail.tsx +4 −2
  14. executor/apps/web/src/lib/session-context-derivation.ts +1 −1
  15. executor/apps/web/src/lib/tools-source-helpers.ts +20 −2
  16. executor/packages/convex/_generated/api.d.ts +313 −317
  17. executor/packages/convex/auth/accounts.ts +5 −5
  18. executor/packages/convex/auth/bootstrap.ts +1 −1
  19. executor/packages/convex/auth/identity.test.ts +47 −0
  20. executor/packages/convex/auth/identity.ts +14 −12
  21. executor/packages/convex/auth/naming.ts +5 −5
  22. executor/packages/convex/auth/personal_workspace.ts +1 −1
  23. executor/packages/convex/billing.ts +1 −1
  24. executor/packages/convex/database.test.ts +0 −1
  25. executor/packages/convex/database/mappers.ts +0 −1
  26. executor/packages/convex/database/policies.ts +2 −12
  27. executor/packages/convex/database/tasks.ts +0 −2
  28. executor/packages/convex/executor.ts +5 −4
  29. executor/packages/convex/http/mcp_service.ts +2 −1
  30. executor/packages/convex/runtime/task_runner.ts +28 −13
  31. executor/packages/convex/runtime/workspace_tools.ts +1 −2
  32. executor/packages/convex/runtimeCallbacks.ts +0 −2
  33. executor/packages/convex/schema.ts +1 −2
  34. executor/packages/convex/task/finish.ts +0 −1
  35. executor/packages/core/src/mcp-server-contracts.ts +2 −1
  36. executor/packages/core/src/mcp-server-formatting.ts +3 −3
  37. executor/packages/core/src/mcp-server.ts +5 −4
  38. executor/packages/core/src/runtimes/cloudflare-worker-loader-runtime.test.ts +29 −0
  39. executor/packages/core/src/runtimes/runtime-catalog.ts +53 −0
  40. executor/packages/core/src/tools.ts +0 −82
  41. executor/packages/core/src/types.ts +6 −0
  42. executor/scripts/prod/doctor-prod.test.ts +26 −0
  43. executor/scripts/prod/doctor-prod.ts +28 −0
  44. executor/scripts/prod/setup-prod-cloudflare.ts +2 −0
.env.example+4−0
@@ -7,6 +7,10 @@ CONVEX_DEPLOYMENT=dev:your-deployment-slug · 6 unmodified lines
7CONVEX_URL=https://your-deployment.convex.cloud7CONVEX_URL=https://your-deployment.convex.cloud
8CONVEX_SITE_URL=https://your-deployment.convex.site8CONVEX_SITE_URL=https://your-deployment.convex.site
9 9
10# ── Runtime restriction (set in Convex prod) ──
11# Set to 1 to force task execution to Cloudflare dynamic worker runtime only.
12# EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY=1
13
10# ── WorkOS (optional — enables auth, omit for guest-only mode) ──14# ── WorkOS (optional — enables auth, omit for guest-only mode) ──
11# WORKOS_CLIENT_ID=client_...15# WORKOS_CLIENT_ID=client_...
12# WORKOS_API_KEY=sk_test_...16# WORKOS_API_KEY=sk_test_...
executor/README.md+1−0
@@ -136,6 +136,7 @@ Important env vars (see root `.env.example` for the base template): · 135 unmodified lines
136- Core:136- Core:
137 - `CONVEX_URL`137 - `CONVEX_URL`
138 - `CONVEX_SITE_URL`138 - `CONVEX_SITE_URL`
139 - `EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY` (`1` in production to force Cloudflare dynamic worker runtime)
139- WorkOS (optional auth/org features):140- WorkOS (optional auth/org features):
140 - `WORKOS_CLIENT_ID`141 - `WORKOS_CLIENT_ID`
141 - `WORKOS_API_KEY`142 - `WORKOS_API_KEY`
executor/apps/web/src/app/[[...dashboard]]/page.tsxrenamed from executor/apps/web/src/app/approvals/page.tsx+1−1
@@ -1,5 +1,5 @@
1import { StaticAppShell } from "@/components/static-app-shell";1import { StaticAppShell } from "@/components/static-app-shell";
2 2
3export default function ApprovalsPage() {3export default function DashboardRoutesPage() {
4 return <StaticAppShell />;4 return <StaticAppShell />;
5}5}
executor/apps/web/src/app/billing/page.tsx+0−5
@@ -1,5 +0,0 @@
1import { redirect } from "next/navigation";
2
3export default function BillingPage() {
4 redirect("/organization?tab=billing");
5}
executor/apps/web/src/app/members/page.tsx+0−5
@@ -1,5 +0,0 @@
1import { redirect } from "next/navigation";
2
3export default function MembersPage() {
4 redirect("/organization?tab=members");
5}
executor/apps/web/src/app/onboarding/page.tsx+0−5
@@ -1,5 +0,0 @@
1import { StaticAppShell } from "@/components/static-app-shell";
2
3export default function OnboardingPage() {
4 return <StaticAppShell />;
5}
executor/apps/web/src/app/organization/page.tsx+0−5
@@ -1,5 +0,0 @@
1import { StaticAppShell } from "@/components/static-app-shell";
2
3export default function OrganizationPage() {
4 return <StaticAppShell />;
5}
executor/apps/web/src/app/page.tsx+0−5
@@ -1,5 +0,0 @@
1import { StaticAppShell } from "@/components/static-app-shell";
2
3export default function DashboardPage() {
4 return <StaticAppShell />;
5}
executor/apps/web/src/app/static-app-shell/page.tsx+0−5
@@ -1,5 +0,0 @@
1import { StaticAppShell } from "@/components/static-app-shell";
2
3export default function StaticAppShellPage() {
4 return <StaticAppShell />;
5}
executor/apps/web/src/app/tasks/page.tsx+0−5
@@ -1,5 +0,0 @@
1import { StaticAppShell } from "@/components/static-app-shell";
2
3export default function TasksPage() {
4 return <StaticAppShell />;
5}
executor/apps/web/src/app/tools/page.tsx+0−5
@@ -1,5 +0,0 @@
1import { StaticAppShell } from "@/components/static-app-shell";
2
3export default function ToolsPage() {
4 return <StaticAppShell />;
5}
executor/apps/web/src/components/tasks/task-composer.tsx+20−15
@@ -1,6 +1,6 @@
1"use client";1"use client";
2 2
3import { useState } from "react";3import { useEffect, useState } from "react";
4import { Play, Send } from "lucide-react";4import { Play, Send } from "lucide-react";
5import { useMutation, useQuery } from "convex/react";5import { useMutation, useQuery } from "convex/react";
6import { toast } from "sonner";6import { toast } from "sonner";
@@ -21,20 +21,12 @@ import { useSession } from "@/lib/session-context"; · 14 unmodified lines
21import type { RuntimeTargetDescriptor } from "@/lib/types";21import type { RuntimeTargetDescriptor } from "@/lib/types";
22import { useWorkspaceTools } from "@/hooks/use-workspace-tools";22import { useWorkspaceTools } from "@/hooks/use-workspace-tools";
23 23
24const DEFAULT_CODE = `// Example: call some tools and return a compact result24const DEFAULT_CODE = `// Example: discover tools and return matching tool names
25const time = await tools.utils.get_time();25const found = await tools.discover({
26const sum = await tools.math.add({ a: 7, b: 35 });26 query: "Discover",
27
28// This will require approval:
29await tools.admin.send_announcement({
30 channel: "general",
31 message: "Hello from executor!"
32});27});
33 28
34return {29return found.results.map((tool) => tool.path);`;
35 isoTime: time.iso,
36 total: sum.result,
37};`;
38const DEFAULT_TIMEOUT_MS = 300_000;30const DEFAULT_TIMEOUT_MS = 300_000;
39 31
40export function TaskComposer() {32export function TaskComposer() {
@@ -47,14 +39,27 @@ export function TaskComposer() { · 6 unmodified lines
47 const runtimes = useQuery(convexApi.workspace.listRuntimeTargets, {});39 const runtimes = useQuery(convexApi.workspace.listRuntimeTargets, {});
48 const createTask = useMutation(convexApi.executor.createTask);40 const createTask = useMutation(convexApi.executor.createTask);
49 const { tools, dtsUrls, loadingTools, loadingTypes } = useWorkspaceTools(context ?? null);41 const { tools, dtsUrls, loadingTools, loadingTypes } = useWorkspaceTools(context ?? null);
42 const runtimeTargets = runtimes ?? [];
43
44 useEffect(() => {
45 if (runtimeTargets.length === 0) {
46 return;
47 }
48 if (!runtimeTargets.some((runtime: RuntimeTargetDescriptor) => runtime.id === runtimeId)) {
49 setRuntimeId(runtimeTargets[0]!.id);
50 }
51 }, [runtimeId, runtimeTargets]);
50 52
51 const handleSubmit = async () => {53 const handleSubmit = async () => {
52 if (!context || !code.trim()) return;54 if (!context || !code.trim()) return;
53 setSubmitting(true);55 setSubmitting(true);
54 try {56 try {
57 const selectedRuntimeId = runtimeTargets.some((runtime: RuntimeTargetDescriptor) => runtime.id === runtimeId)
58 ? runtimeId
59 : undefined;
55 const data = await createTask({60 const data = await createTask({
56 code,61 code,
57 runtimeId,62 runtimeId: selectedRuntimeId,
58 timeoutMs: Number.parseInt(timeoutMs, 10) || DEFAULT_TIMEOUT_MS,63 timeoutMs: Number.parseInt(timeoutMs, 10) || DEFAULT_TIMEOUT_MS,
59 workspaceId: context.workspaceId,64 workspaceId: context.workspaceId,
60 sessionId: context.sessionId,65 sessionId: context.sessionId,
@@ -87,7 +92,7 @@ export function TaskComposer() { · 26 unmodified lines
87 <SelectValue />92 <SelectValue />
88 </SelectTrigger>93 </SelectTrigger>
89 <SelectContent>94 <SelectContent>
90 {(runtimes ?? []).map((r: RuntimeTargetDescriptor) => (95 {runtimeTargets.map((r: RuntimeTargetDescriptor) => (
91 <SelectItem key={r.id} value={r.id} className="text-xs">96 <SelectItem key={r.id} value={r.id} className="text-xs">
92 {r.label}97 {r.label}
93 </SelectItem>98 </SelectItem>
executor/apps/web/src/components/tools/explorer-tool-detail.tsx+4−2
@@ -11,6 +11,8 @@ const codePlugin = createCodePlugin({ · 10 unmodified lines
11 11
12export function ToolDetail({ tool, depth }: { tool: ToolDescriptor; depth: number }) {12export function ToolDetail({ tool, depth }: { tool: ToolDescriptor; depth: number }) {
13 const insetLeft = depth * 20 + 8 + 16 + 8;13 const insetLeft = depth * 20 + 8 + 16 + 8;
14 const argsType = tool.strictArgsType?.trim() || tool.argsType;
15 const returnsType = tool.strictReturnsType?.trim() || tool.returnsType;
14 16
15 return (17 return (
16 <div className="space-y-2.5 pb-3 pt-1 pr-2" style={{ paddingLeft: insetLeft }}>18 <div className="space-y-2.5 pb-3 pt-1 pr-2" style={{ paddingLeft: insetLeft }}>
@@ -36,8 +38,8 @@ export function ToolDetail({ tool, depth }: { tool: ToolDescriptor; depth: numbe · 19 unmodified lines
36 )}38 )}
37 </div>39 </div>
38 40
39 {tool.argsType && <TypeSignature raw={tool.argsType} label="Arguments" />}41 {argsType && <TypeSignature raw={argsType} label="Arguments" />}
40 {tool.returnsType && <TypeSignature raw={tool.returnsType} label="Returns" />}42 {returnsType && <TypeSignature raw={returnsType} label="Returns" />}
41 </div>43 </div>
42 );44 );
43}45}
executor/apps/web/src/lib/session-context-derivation.ts+1−1
@@ -5,7 +5,7 @@ export interface WorkosAccount { · 4 unmodified lines
5 _id: Id<"accounts">;5 _id: Id<"accounts">;
6 provider: "workos" | "anonymous";6 provider: "workos" | "anonymous";
7 providerAccountId: string;7 providerAccountId: string;
8 email: string;8 email?: string;
9 name: string;9 name: string;
10 avatarUrl?: string | null;10 avatarUrl?: string | null;
11}11}
executor/apps/web/src/lib/tools-source-helpers.ts+20−2
@@ -271,8 +271,26 @@ export function withUniqueSourceName(baseName: string, takenNames: Set<string>): · 270 unmodified lines
271 }271 }
272}272}
273 273
274export function catalogSourceName(item: { providerName: string; name: string }): string {274function sourceNameFromCatalogId(id: string): string {
275 let slug = sanitizeSourceName(id);
276 slug = slug
277 .replace(/-openapi$/, "")
278 .replace(/-graphql-api$/, "")
279 .replace(/-rest-api$/, "")
280 .replace(/-api$/, "")
281 .replace(/-rest$/, "");
282 return slug || "catalog";
283}
284
285export function catalogSourceName(item: { id?: string; providerName: string; name: string }): string {
286 if (item.id && item.id.trim().length > 0) {
287 return sourceNameFromCatalogId(item.id);
288 }
289
275 const owner = sanitizeSourceName(item.providerName || "catalog");290 const owner = sanitizeSourceName(item.providerName || "catalog");
276 const title = sanitizeSourceName(item.name);291 const title = sanitizeSourceName(item.name);
277 return sanitizeSourceName(`${owner}-${title}`);292 if (!title || title === "api" || title === "openapi" || title === "graphql") {
293 return owner;
294 }
295 return title;
278}296}
executor/packages/convex/_generated/api.d.ts+313−317
@@ -326,7 +326,6 @@ export declare const api: { · 325 unmodified lines
326 error?: string;326 error?: string;
327 exitCode?: number;327 exitCode?: number;
328 internalSecret: string;328 internalSecret: string;
329 result?: any;
330 runId: string;329 runId: string;
331 status: "completed" | "failed" | "timed_out" | "denied";330 status: "completed" | "failed" | "timed_out" | "denied";
332 },331 },
@@ -557,6 +556,60 @@ export declare const internal: { · 224 unmodified lines
557 >;556 >;
558 };557 };
559 database: {558 database: {
559 anonymous_session: {
560 bootstrapAnonymousSession: FunctionReference<
561 "mutation",
562 "internal",
563 { sessionId?: string },
564 any
565 >;
566 };
567 approvals: {
568 createApproval: FunctionReference<
569 "mutation",
570 "internal",
571 { id: string; input?: any; taskId: string; toolPath: string },
572 any
573 >;
574 getApproval: FunctionReference<
575 "query",
576 "internal",
577 { approvalId: string },
578 any
579 >;
580 getApprovalInWorkspace: FunctionReference<
581 "query",
582 "internal",
583 { approvalId: string; workspaceId: Id<"workspaces"> },
584 any
585 >;
586 listApprovals: FunctionReference<
587 "query",
588 "internal",
589 {
590 status?: "pending" | "approved" | "denied";
591 workspaceId: Id<"workspaces">;
592 },
593 any
594 >;
595 listPendingApprovals: FunctionReference<
596 "query",
597 "internal",
598 { workspaceId: Id<"workspaces"> },
599 any
600 >;
601 resolveApproval: FunctionReference<
602 "mutation",
603 "internal",
604 {
605 approvalId: string;
606 decision: "approved" | "denied";
607 reason?: string;
608 reviewerId?: string;
609 },
610 any
611 >;
612 };
560 bootstrapAnonymousSession: FunctionReference<613 bootstrapAnonymousSession: FunctionReference<
561 "mutation",614 "mutation",
562 "internal",615 "internal",
@@ -602,6 +655,41 @@ export declare const internal: { · 39 unmodified lines
602 { eventName: string; payload: any; taskId: string; type: string },655 { eventName: string; payload: any; taskId: string; type: string },
603 any656 any
604 >;657 >;
658 credentials: {
659 listCredentialProviders: FunctionReference<"query", "internal", {}, any>;
660 listCredentials: FunctionReference<
661 "query",
662 "internal",
663 { workspaceId: Id<"workspaces"> },
664 any
665 >;
666 resolveCredential: FunctionReference<
667 "query",
668 "internal",
669 {
670 actorId?: string;
671 scope: "workspace" | "actor";
672 sourceKey: string;
673 workspaceId: Id<"workspaces">;
674 },
675 any
676 >;
677 upsertCredential: FunctionReference<
678 "mutation",
679 "internal",
680 {
681 actorId?: string;
682 id?: string;
683 overridesJson?: any;
684 provider?: "local-convex" | "workos-vault";
685 scope: "workspace" | "actor";
686 secretJson: any;
687 sourceKey: string;
688 workspaceId: Id<"workspaces">;
689 },
690 any
691 >;
692 };
605 deleteToolSource: FunctionReference<693 deleteToolSource: FunctionReference<
606 "mutation",694 "mutation",
607 "internal",695 "internal",
@@ -721,7 +809,6 @@ export declare const internal: { · 113 unmodified lines
721 {809 {
722 error?: string;810 error?: string;
723 exitCode?: number;811 exitCode?: number;
724 result?: any;
725 status: "completed" | "failed" | "timed_out" | "denied";812 status: "completed" | "failed" | "timed_out" | "denied";
726 taskId: string;813 taskId: string;
727 },814 },
@@ -733,6 +820,94 @@ export declare const internal: { · 5 unmodified lines
733 { taskId: string },820 { taskId: string },
734 any821 any
735 >;822 >;
823 oauth: {
824 consumeAnonymousOauthAuthorizationCode: FunctionReference<
825 "mutation",
826 "internal",
827 { code: string },
828 any
829 >;
830 countAnonymousOauthAuthorizationCodes: FunctionReference<
831 "query",
832 "internal",
833 {},
834 any
835 >;
836 getActiveAnonymousOauthSigningKey: FunctionReference<
837 "query",
838 "internal",
839 {},
840 any
841 >;
842 getAnonymousOauthClient: FunctionReference<
843 "query",
844 "internal",
845 { clientId: string },
846 any
847 >;
848 purgeExpiredAnonymousOauthAuthorizationCodes: FunctionReference<
849 "mutation",
850 "internal",
851 { now: number },
852 any
853 >;
854 registerAnonymousOauthClient: FunctionReference<
855 "mutation",
856 "internal",
857 { clientId: string; clientName?: string; redirectUris: Array<string> },
858 any
859 >;
860 storeAnonymousOauthAuthorizationCode: FunctionReference<
861 "mutation",
862 "internal",
863 {
864 actorId: string;
865 clientId: string;
866 code: string;
867 codeChallenge: string;
868 codeChallengeMethod: string;
869 createdAt: number;
870 expiresAt: number;
871 redirectUri: string;
872 tokenClaims?: any;
873 },
874 any
875 >;
876 storeAnonymousOauthSigningKey: FunctionReference<
877 "mutation",
878 "internal",
879 {
880 algorithm: string;
881 keyId: string;
882 privateKeyJwk: any;
883 publicKeyJwk: any;
884 },
885 any
886 >;
887 };
888 policies: {
889 listAccessPolicies: FunctionReference<
890 "query",
891 "internal",
892 { workspaceId: Id<"workspaces"> },
893 any
894 >;
895 listRuntimeTargets: FunctionReference<"query", "internal", {}, any>;
896 upsertAccessPolicy: FunctionReference<
897 "mutation",
898 "internal",
899 {
900 actorId?: string;
901 clientId?: string;
902 decision: "allow" | "require_approval" | "deny";
903 id?: string;
904 priority?: number;
905 toolPathPattern: string;
906 workspaceId: Id<"workspaces">;
907 },
908 any
909 >;
910 };
736 purgeExpiredAnonymousOauthAuthorizationCodes: FunctionReference<911 purgeExpiredAnonymousOauthAuthorizationCodes: FunctionReference<
737 "mutation",912 "mutation",
738 "internal",913 "internal",
@@ -800,6 +975,142 @@ export declare const internal: { · 61 unmodified lines
800 },975 },
801 any976 any
802 >;977 >;
978 task_events: {
979 createTaskEvent: FunctionReference<
980 "mutation",
981 "internal",
982 { eventName: string; payload: any; taskId: string; type: string },
983 any
984 >;
985 listTaskEvents: FunctionReference<
986 "query",
987 "internal",
988 { taskId: string },
989 any
990 >;
991 };
992 tasks: {
993 createTask: FunctionReference<
994 "mutation",
995 "internal",
996 {
997 actorId: string;
998 clientId?: string;
999 code: string;
1000 id: string;
1001 metadata?: any;
1002 runtimeId: string;
1003 timeoutMs?: number;
1004 workspaceId: Id<"workspaces">;
1005 },
1006 any
1007 >;
1008 getTask: FunctionReference<"query", "internal", { taskId: string }, any>;
1009 getTaskInWorkspace: FunctionReference<
1010 "query",
1011 "internal",
1012 { taskId: string; workspaceId: Id<"workspaces"> },
1013 any
1014 >;
1015 listQueuedTaskIds: FunctionReference<
1016 "query",
1017 "internal",
1018 { limit?: number },
1019 any
1020 >;
1021 listTasks: FunctionReference<
1022 "query",
1023 "internal",
1024 { workspaceId: Id<"workspaces"> },
1025 any
1026 >;
1027 markTaskFinished: FunctionReference<
1028 "mutation",
1029 "internal",
1030 {
1031 error?: string;
1032 exitCode?: number;
1033 status: "completed" | "failed" | "timed_out" | "denied";
1034 taskId: string;
1035 },
1036 any
1037 >;
1038 markTaskRunning: FunctionReference<
1039 "mutation",
1040 "internal",
1041 { taskId: string },
1042 any
1043 >;
1044 };
1045 tool_calls: {
1046 finishToolCall: FunctionReference<
1047 "mutation",
1048 "internal",
1049 {
1050 callId: string;
1051 error?: string;
1052 status: "completed" | "failed" | "denied";
1053 taskId: string;
1054 },
1055 any
1056 >;
1057 getToolCall: FunctionReference<
1058 "query",
1059 "internal",
1060 { callId: string; taskId: string },
1061 any
1062 >;
1063 listToolCalls: FunctionReference<
1064 "query",
1065 "internal",
1066 { taskId: string },
1067 any
1068 >;
1069 setToolCallPendingApproval: FunctionReference<
1070 "mutation",
1071 "internal",
1072 { approvalId: string; callId: string; taskId: string },
1073 any
1074 >;
1075 upsertToolCallRequested: FunctionReference<
1076 "mutation",
1077 "internal",
1078 {
1079 callId: string;
1080 taskId: string;
1081 toolPath: string;
1082 workspaceId: Id<"workspaces">;
1083 },
1084 any
1085 >;
1086 };
1087 tool_sources: {
1088 deleteToolSource: FunctionReference<
1089 "mutation",
1090 "internal",
1091 { sourceId: string; workspaceId: Id<"workspaces"> },
1092 any
1093 >;
1094 listToolSources: FunctionReference<
1095 "query",
1096 "internal",
1097 { workspaceId: Id<"workspaces"> },
1098 any
1099 >;
1100 upsertToolSource: FunctionReference<
1101 "mutation",
1102 "internal",
1103 {
1104 config: any;
1105 enabled?: boolean;
1106 id?: string;
1107 name: string;
1108 type: "mcp" | "openapi" | "graphql";
1109 workspaceId: Id<"workspaces">;
1110 },
1111 any
1112 >;
1113 };
803 upsertAccessPolicy: FunctionReference<1114 upsertAccessPolicy: FunctionReference<
804 "mutation",1115 "mutation",
805 "internal",1116 "internal",
@@ -854,320 +1165,6 @@ export declare const internal: { · 48 unmodified lines
854 any1165 any
855 >;1166 >;
856 };1167 };
857 database_anonymous_session: {
858 bootstrapAnonymousSession: FunctionReference<
859 "mutation",
860 "internal",
861 { sessionId?: string },
862 any
863 >;
864 };
865 database_approvals: {
866 createApproval: FunctionReference<
867 "mutation",
868 "internal",
869 { id: string; input?: any; taskId: string; toolPath: string },
870 any
871 >;
872 getApproval: FunctionReference<
873 "query",
874 "internal",
875 { approvalId: string },
876 any
877 >;
878 getApprovalInWorkspace: FunctionReference<
879 "query",
880 "internal",
881 { approvalId: string; workspaceId: Id<"workspaces"> },
882 any
883 >;
884 listApprovals: FunctionReference<
885 "query",
886 "internal",
887 {
888 status?: "pending" | "approved" | "denied";
889 workspaceId: Id<"workspaces">;
890 },
891 any
892 >;
893 listPendingApprovals: FunctionReference<
894 "query",
895 "internal",
896 { workspaceId: Id<"workspaces"> },
897 any
898 >;
899 resolveApproval: FunctionReference<
900 "mutation",
901 "internal",
902 {
903 approvalId: string;
904 decision: "approved" | "denied";
905 reason?: string;
906 reviewerId?: string;
907 },
908 any
909 >;
910 };
911 database_credentials: {
912 listCredentialProviders: FunctionReference<"query", "internal", {}, any>;
913 listCredentials: FunctionReference<
914 "query",
915 "internal",
916 { workspaceId: Id<"workspaces"> },
917 any
918 >;
919 resolveCredential: FunctionReference<
920 "query",
921 "internal",
922 {
923 actorId?: string;
924 scope: "workspace" | "actor";
925 sourceKey: string;
926 workspaceId: Id<"workspaces">;
927 },
928 any
929 >;
930 upsertCredential: FunctionReference<
931 "mutation",
932 "internal",
933 {
934 actorId?: string;
935 id?: string;
936 overridesJson?: any;
937 provider?: "local-convex" | "workos-vault";
938 scope: "workspace" | "actor";
939 secretJson: any;
940 sourceKey: string;
941 workspaceId: Id<"workspaces">;
942 },
943 any
944 >;
945 };
946 database_oauth: {
947 consumeAnonymousOauthAuthorizationCode: FunctionReference<
948 "mutation",
949 "internal",
950 { code: string },
951 any
952 >;
953 countAnonymousOauthAuthorizationCodes: FunctionReference<
954 "query",
955 "internal",
956 {},
957 any
958 >;
959 getActiveAnonymousOauthSigningKey: FunctionReference<
960 "query",
961 "internal",
962 {},
963 any
964 >;
965 getAnonymousOauthClient: FunctionReference<
966 "query",
967 "internal",
968 { clientId: string },
969 any
970 >;
971 purgeExpiredAnonymousOauthAuthorizationCodes: FunctionReference<
972 "mutation",
973 "internal",
974 { now: number },
975 any
976 >;
977 registerAnonymousOauthClient: FunctionReference<
978 "mutation",
979 "internal",
980 { clientId: string; clientName?: string; redirectUris: Array<string> },
981 any
982 >;
983 storeAnonymousOauthAuthorizationCode: FunctionReference<
984 "mutation",
985 "internal",
986 {
987 actorId: string;
988 clientId: string;
989 code: string;
990 codeChallenge: string;
991 codeChallengeMethod: string;
992 createdAt: number;
993 expiresAt: number;
994 redirectUri: string;
995 tokenClaims?: any;
996 },
997 any
998 >;
999 storeAnonymousOauthSigningKey: FunctionReference<
1000 "mutation",
1001 "internal",
1002 {
1003 algorithm: string;
1004 keyId: string;
1005 privateKeyJwk: any;
1006 publicKeyJwk: any;
1007 },
1008 any
1009 >;
1010 };
1011 database_policies: {
1012 listAccessPolicies: FunctionReference<
1013 "query",
1014 "internal",
1015 { workspaceId: Id<"workspaces"> },
1016 any
1017 >;
1018 listRuntimeTargets: FunctionReference<"query", "internal", {}, any>;
1019 upsertAccessPolicy: FunctionReference<
1020 "mutation",
1021 "internal",
1022 {
1023 actorId?: string;
1024 clientId?: string;
1025 decision: "allow" | "require_approval" | "deny";
1026 id?: string;
1027 priority?: number;
1028 toolPathPattern: string;
1029 workspaceId: Id<"workspaces">;
1030 },
1031 any
1032 >;
1033 };
1034 database_task_events: {
1035 createTaskEvent: FunctionReference<
1036 "mutation",
1037 "internal",
1038 { eventName: string; payload: any; taskId: string; type: string },
1039 any
1040 >;
1041 listTaskEvents: FunctionReference<
1042 "query",
1043 "internal",
1044 { taskId: string },
1045 any
1046 >;
1047 };
1048 database_tasks: {
1049 createTask: FunctionReference<
1050 "mutation",
1051 "internal",
1052 {
1053 actorId: string;
1054 clientId?: string;
1055 code: string;
1056 id: string;
1057 metadata?: any;
1058 runtimeId: string;
1059 timeoutMs?: number;
1060 workspaceId: Id<"workspaces">;
1061 },
1062 any
1063 >;
1064 getTask: FunctionReference<"query", "internal", { taskId: string }, any>;
1065 getTaskInWorkspace: FunctionReference<
1066 "query",
1067 "internal",
1068 { taskId: string; workspaceId: Id<"workspaces"> },
1069 any
1070 >;
1071 listQueuedTaskIds: FunctionReference<
1072 "query",
1073 "internal",
1074 { limit?: number },
1075 any
1076 >;
1077 listTasks: FunctionReference<
1078 "query",
1079 "internal",
1080 { workspaceId: Id<"workspaces"> },
1081 any
1082 >;
1083 markTaskFinished: FunctionReference<
1084 "mutation",
1085 "internal",
1086 {
1087 error?: string;
1088 exitCode?: number;
1089 result?: any;
1090 status: "completed" | "failed" | "timed_out" | "denied";
1091 taskId: string;
1092 },
1093 any
1094 >;
1095 markTaskRunning: FunctionReference<
1096 "mutation",
1097 "internal",
1098 { taskId: string },
1099 any
1100 >;
1101 };
1102 database_tool_calls: {
1103 finishToolCall: FunctionReference<
1104 "mutation",
1105 "internal",
1106 {
1107 callId: string;
1108 error?: string;
1109 status: "completed" | "failed" | "denied";
1110 taskId: string;
1111 },
1112 any
1113 >;
1114 getToolCall: FunctionReference<
1115 "query",
1116 "internal",
1117 { callId: string; taskId: string },
1118 any
1119 >;
1120 listToolCalls: FunctionReference<
1121 "query",
1122 "internal",
1123 { taskId: string },
1124 any
1125 >;
1126 setToolCallPendingApproval: FunctionReference<
1127 "mutation",
1128 "internal",
1129 { approvalId: string; callId: string; taskId: string },
1130 any
1131 >;
1132 upsertToolCallRequested: FunctionReference<
1133 "mutation",
1134 "internal",
1135 {
1136 callId: string;
1137 taskId: string;
1138 toolPath: string;
1139 workspaceId: Id<"workspaces">;
1140 },
1141 any
1142 >;
1143 };
1144 database_tool_sources: {
1145 deleteToolSource: FunctionReference<
1146 "mutation",
1147 "internal",
1148 { sourceId: string; workspaceId: Id<"workspaces"> },
1149 any
1150 >;
1151 listToolSources: FunctionReference<
1152 "query",
1153 "internal",
1154 { workspaceId: Id<"workspaces"> },
1155 any
1156 >;
1157 upsertToolSource: FunctionReference<
1158 "mutation",
1159 "internal",
1160 {
1161 config: any;
1162 enabled?: boolean;
1163 id?: string;
1164 name: string;
1165 type: "mcp" | "openapi" | "graphql";
1166 workspaceId: Id<"workspaces">;
1167 },
1168 any
1169 >;
1170 };
1171 executor: {1168 executor: {
1172 completeRuntimeRun: FunctionReference<1169 completeRuntimeRun: FunctionReference<
1173 "mutation",1170 "mutation",
@@ -1176,7 +1173,6 @@ export declare const internal: { · 2 unmodified lines
1176 durationMs?: number;1173 durationMs?: number;
1177 error?: string;1174 error?: string;
1178 exitCode?: number;1175 exitCode?: number;
1179 result?: any;
1180 runId: string;1176 runId: string;
1181 status: "completed" | "failed" | "timed_out" | "denied";1177 status: "completed" | "failed" | "timed_out" | "denied";
1182 },1178 },
executor/packages/convex/auth/accounts.ts+5−5
@@ -3,7 +3,7 @@ import type { DbCtx } from "./types"; · 2 unmodified lines
3 3
4type UpsertWorkosAccountArgs = {4type UpsertWorkosAccountArgs = {
5 workosUserId: string;5 workosUserId: string;
6 email: string;6 email?: string;
7 fullName: string;7 fullName: string;
8 firstName?: string;8 firstName?: string;
9 lastName?: string;9 lastName?: string;
@@ -15,13 +15,13 @@ type UpsertWorkosAccountArgs = { · 5 unmodified lines
15export async function upsertWorkosAccount(ctx: DbCtx, args: UpsertWorkosAccountArgs) {15export async function upsertWorkosAccount(ctx: DbCtx, args: UpsertWorkosAccountArgs) {
16 let account = await getAccountByWorkosId(ctx, args.workosUserId);16 let account = await getAccountByWorkosId(ctx, args.workosUserId);
17 const patchData = {17 const patchData = {
18 email: args.email,
19 name: args.fullName,18 name: args.fullName,
20 firstName: args.firstName,
21 lastName: args.lastName,
22 avatarUrl: args.avatarUrl,
23 status: "active" as const,19 status: "active" as const,
24 updatedAt: args.now,20 updatedAt: args.now,
21 ...(args.email ? { email: args.email } : null),
22 ...(args.firstName !== undefined ? { firstName: args.firstName } : null),
23 ...(args.lastName !== undefined ? { lastName: args.lastName } : null),
24 ...(args.avatarUrl !== undefined ? { avatarUrl: args.avatarUrl } : null),
25 ...(args.includeLastLoginAt ? { lastLoginAt: args.now } : null),25 ...(args.includeLastLoginAt ? { lastLoginAt: args.now } : null),
26 };26 };
27 27
executor/packages/convex/auth/bootstrap.ts+1−1
@@ -16,7 +16,7 @@ async function seedHintedOrganizationMembership( · 15 unmodified lines
16 args: {16 args: {
17 accountId: AccountId;17 accountId: AccountId;
18 hintedWorkosOrgId?: string;18 hintedWorkosOrgId?: string;
19 email: string;19 email?: string;
20 now: number;20 now: number;
21 },21 },
22) {22) {
executor/packages/convex/auth/identity.test.ts+47−0
@@ -0,0 +1,47 @@
1import { expect, test } from "bun:test";
2import { resolveIdentityProfile } from "./identity";
3
4test("resolveIdentityProfile uses stable user label when identity is sparse", () => {
5 const subject = "user_01KH1TVHS4WJCPQG2XQJGMMJMJ";
6
7 const profile = resolveIdentityProfile({
8 identity: { subject },
9 authKitProfile: null,
10 });
11
12 expect(profile.email).toBeUndefined();
13 expect(profile.fullName).toBe("User GMMJMJ");
14});
15
16test("resolveIdentityProfile reads standard identity claims", () => {
17 const profile = resolveIdentityProfile({
18 identity: {
19 subject: "user_abc123",
20 email: "alex@example.com",
21 given_name: "Alex",
22 family_name: "Doe",
23 picture: "https://cdn.example.com/alex.png",
24 organization_id: "org_123",
25 },
26 authKitProfile: null,
27 });
28
29 expect(profile.email).toBe("alex@example.com");
30 expect(profile.firstName).toBe("Alex");
31 expect(profile.lastName).toBe("Doe");
32 expect(profile.fullName).toBe("Alex Doe");
33 expect(profile.avatarUrl).toBe("https://cdn.example.com/alex.png");
34 expect(profile.hintedWorkosOrgId).toBe("org_123");
35});
36
37test("resolveIdentityProfile prefers explicit identity name", () => {
38 const profile = resolveIdentityProfile({
39 identity: {
40 subject: "user_abc123",
41 name: "Alexandra D.",
42 },
43 authKitProfile: null,
44 });
45
46 expect(profile.fullName).toBe("Alexandra D.");
47});
executor/packages/convex/auth/identity.ts+14−12
@@ -8,6 +8,10 @@ type WorkosProfile = { · 7 unmodified lines
8 profilePictureUrl?: string | null;8 profilePictureUrl?: string | null;
9};9};
10 10
11function deriveFallbackUserLabel(workosUserId: string): string {
12 return `User ${workosUserId.slice(-6)}`;
13}
14
11export function getIdentityString(identity: Record<string, unknown>, keys: string[]): string | undefined {15export function getIdentityString(identity: Record<string, unknown>, keys: string[]): string | undefined {
12 for (const key of keys) {16 for (const key of keys) {
13 const value = identity[key];17 const value = identity[key];
@@ -36,28 +40,26 @@ export function resolveIdentityProfile(args: { · 22 unmodified lines
36 40
37 const email =41 const email =
38 authKitProfile?.email42 authKitProfile?.email
39 ?? getIdentityString(identity, ["email", "https://workos.com/email", "upn"])43 ?? getIdentityString(identity, ["email", "upn"]);
40 ?? `${identity.subject}@workos.executor.local`;
41 44
42 const firstName = authKitProfile?.firstName45 const firstName = authKitProfile?.firstName
43 ?? getIdentityString(identity, ["given_name", "first_name", "https://workos.com/first_name"]);46 ?? getIdentityString(identity, ["given_name", "first_name"]);
44 47
45 const lastName = authKitProfile?.lastName48 const lastName = authKitProfile?.lastName
46 ?? getIdentityString(identity, ["family_name", "last_name", "https://workos.com/last_name"]);49 ?? getIdentityString(identity, ["family_name", "last_name"]);
50
51 const combinedName = [firstName, lastName].filter(Boolean).join(" ").trim();
47 52
48 const fullName =53 const fullName =
49 (getIdentityString(identity, ["name", "https://workos.com/name"]) ?? [firstName, lastName].filter(Boolean).join(" "))54 getIdentityString(identity, ["name", "full_name"])
50 || email;55 ?? (combinedName.length > 0 ? combinedName : undefined)
56 ?? deriveFallbackUserLabel(identity.subject);
51 57
52 const avatarUrl =58 const avatarUrl =
53 (authKitProfile?.profilePictureUrl ?? undefined)59 (authKitProfile?.profilePictureUrl ?? undefined)
54 ?? getIdentityString(identity, ["picture", "avatar_url", "https://workos.com/profile_picture_url"]);60 ?? getIdentityString(identity, ["picture", "avatar_url", "profile_picture_url"]);
55 61
56 const hintedWorkosOrgId = getIdentityString(identity, [62 const hintedWorkosOrgId = getIdentityString(identity, ["org_id", "organization_id"]);
57 "org_id",
58 "organization_id",
59 "https://workos.com/organization_id",
60 ]);
61 63
62 return {64 return {
63 email,65 email,
executor/packages/convex/auth/naming.ts+5−5
@@ -38,7 +38,7 @@ export function isGeneratedPersonalWorkspaceName(name: string, workosUserId: str · 37 unmodified lines
38 return new RegExp(`^${escapeRegex(workosUserId)}'s workspace$`, "i").test(name);38 return new RegExp(`^${escapeRegex(workosUserId)}'s workspace$`, "i").test(name);
39}39}
40 40
41function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email: string; workosUserId: string }): string {41function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email?: string; workosUserId: string }): string {
42 const firstName = args.firstName?.trim();42 const firstName = args.firstName?.trim();
43 if (firstName && !/^my$/i.test(firstName)) {43 if (firstName && !/^my$/i.test(firstName)) {
44 return firstName;44 return firstName;
@@ -49,7 +49,7 @@ function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email: · 4 unmodified lines
49 return fullName;49 return fullName;
50 }50 }
51 51
52 const emailLocalPart = args.email.split("@")[0]?.trim();52 const emailLocalPart = args.email?.split("@")[0]?.trim();
53 if (emailLocalPart) {53 if (emailLocalPart) {
54 const normalized = emailLocalPart54 const normalized = emailLocalPart
55 .replace(/[._-]+/g, " ")55 .replace(/[._-]+/g, " ")
@@ -64,7 +64,7 @@ function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email: · 8 unmodified lines
64 return `User ${args.workosUserId.slice(-6)}`;64 return `User ${args.workosUserId.slice(-6)}`;
65}65}
66 66
67export function derivePersonalNames(args: { firstName?: string; fullName?: string; email: string; workosUserId: string }) {67export function derivePersonalNames(args: { firstName?: string; fullName?: string; email?: string; workosUserId: string }) {
68 const ownerLabel = deriveOwnerLabel(args);68 const ownerLabel = deriveOwnerLabel(args);
69 return {69 return {
70 organizationName: `${ownerLabel}'s Organization`,70 organizationName: `${ownerLabel}'s Organization`,
@@ -72,8 +72,8 @@ export function derivePersonalNames(args: { firstName?: string; fullName?: strin · 1 unmodified line
72 };72 };
73}73}
74 74
75export function buildPersonalWorkspaceSlugSeed(email: string, workosUserId: string): string {75export function buildPersonalWorkspaceSlugSeed(email: string | undefined, workosUserId: string): string {
76 const baseSlug = slugify(email.split("@")[0] ?? workosUserId, "workspace");76 const baseSlug = slugify(email?.split("@")[0] ?? workosUserId, "workspace");
77 return `${baseSlug}-${workosUserId.slice(-6)}`;77 return `${baseSlug}-${workosUserId.slice(-6)}`;
78}78}
79 79
executor/packages/convex/auth/personal_workspace.ts+1−1
@@ -10,7 +10,7 @@ import { · 9 unmodified lines
10import type { AccountId, DbCtx } from "./types";10import type { AccountId, DbCtx } from "./types";
11 11
12type PersonalWorkspaceOptions = {12type PersonalWorkspaceOptions = {
13 email: string;13 email?: string;
14 firstName?: string;14 firstName?: string;
15 fullName?: string;15 fullName?: string;
16 workosUserId: string;16 workosUserId: string;
executor/packages/convex/billing.ts+1−1
@@ -160,7 +160,7 @@ export const createCustomerPortal = action({ · 159 unmodified lines
160 handler: async (ctx, args): Promise<{ url: string }> => {160 handler: async (ctx, args): Promise<{ url: string }> => {
161 const access: {161 const access: {
162 role: string;162 role: string;
163 email: string;163 email?: string;
164 organizationName: string;164 organizationName: string;
165 billableMembers: number;165 billableMembers: number;
166 customerId: string | null;166 customerId: string | null;
executor/packages/convex/database.test.ts+0−1
@@ -59,7 +59,6 @@ test("task lifecycle supports queue, run, and complete", async () => { · 58 unmodified lines
59 const finished = await t.mutation(internal.database.markTaskFinished as any, {59 const finished = await t.mutation(internal.database.markTaskFinished as any, {
60 taskId: "task_1",60 taskId: "task_1",
61 status: "completed",61 status: "completed",
62 result: { ok: true },
63 exitCode: 0,62 exitCode: 0,
64 });63 });
65 expect(finished?.status).toBe("completed");64 expect(finished?.status).toBe("completed");
executor/packages/convex/database/mappers.ts+0−1
@@ -58,7 +58,6 @@ export function mapTask(doc: Doc<"tasks">) { · 57 unmodified lines
58 startedAt: doc.startedAt,58 startedAt: doc.startedAt,
59 completedAt: doc.completedAt,59 completedAt: doc.completedAt,
60 error: doc.error,60 error: doc.error,
61 result: doc.result,
62 exitCode: doc.exitCode,61 exitCode: doc.exitCode,
63 };62 };
64}63}
executor/packages/convex/database/policies.ts+2−12
@@ -1,23 +1,13 @@
1import { v } from "convex/values";1import { v } from "convex/values";
2import { internalMutation, internalQuery } from "../_generated/server";2import { internalMutation, internalQuery } from "../_generated/server";
3import { listRuntimeTargets as listAvailableRuntimeTargets } from "../../core/src/runtimes/runtime-catalog";
3import { mapPolicy } from "./mappers";4import { mapPolicy } from "./mappers";
4import { policyDecisionValidator } from "./validators";5import { policyDecisionValidator } from "./validators";
5 6
6export const listRuntimeTargets = internalQuery({7export const listRuntimeTargets = internalQuery({
7 args: {},8 args: {},
8 handler: async () => {9 handler: async () => {
9 return [10 return listAvailableRuntimeTargets();
10 {
11 id: "local-bun",
12 label: "Local JS Runtime",
13 description: "Runs generated code in-process using Bun",
14 },
15 {
16 id: "cloudflare-worker-loader",
17 label: "Cloudflare Worker Loader",
18 description: "Runs generated code in a Cloudflare Worker",
19 },
20 ];
21 },11 },
22});12});
23 13
executor/packages/convex/database/tasks.ts+0−2
@@ -115,7 +115,6 @@ export const markTaskFinished = internalMutation({ · 114 unmodified lines
115 args: {115 args: {
116 taskId: v.string(),116 taskId: v.string(),
117 status: completedTaskStatusValidator,117 status: completedTaskStatusValidator,
118 result: v.optional(v.any()),
119 exitCode: v.optional(v.number()),118 exitCode: v.optional(v.number()),
120 error: v.optional(v.string()),119 error: v.optional(v.string()),
121 },120 },
@@ -132,7 +131,6 @@ export const markTaskFinished = internalMutation({ · 10 unmodified lines
132 const now = Date.now();131 const now = Date.now();
133 await ctx.db.patch(doc._id, {132 await ctx.db.patch(doc._id, {
134 status: args.status,133 status: args.status,
135 result: args.result,
136 exitCode: args.exitCode,134 exitCode: args.exitCode,
137 error: args.error,135 error: args.error,
138 completedAt: now,136 completedAt: now,
executor/packages/convex/executor.ts+5−4
@@ -5,7 +5,7 @@ import type { MutationCtx } from "./_generated/server"; · 4 unmodified lines
5import { internalMutation } from "./_generated/server";5import { internalMutation } from "./_generated/server";
6import { workspaceMutation } from "../core/src/function-builders";6import { workspaceMutation } from "../core/src/function-builders";
7import { actorIdForAccount } from "../core/src/identity";7import { actorIdForAccount } from "../core/src/identity";
8import { isKnownRuntimeId } from "../core/src/runtimes/runtime-catalog";8import { defaultRuntimeId, isKnownRuntimeId, isRuntimeEnabled } from "../core/src/runtimes/runtime-catalog";
9import type { ApprovalRecord, TaskRecord } from "../core/src/types";9import type { ApprovalRecord, TaskRecord } from "../core/src/types";
10import { isTerminalTaskStatus, taskTerminalEventType } from "./task/status";10import { isTerminalTaskStatus, taskTerminalEventType } from "./task/status";
11import { DEFAULT_TASK_TIMEOUT_MS } from "./task/constants";11import { DEFAULT_TASK_TIMEOUT_MS } from "./task/constants";
@@ -29,10 +29,13 @@ async function createTaskRecord( · 17 unmodified lines
29 throw new Error("Task code is required");29 throw new Error("Task code is required");
30 }30 }
31 31
32 const runtimeId = args.runtimeId ?? "local-bun";32 const runtimeId = args.runtimeId ?? defaultRuntimeId();
33 if (!isKnownRuntimeId(runtimeId)) {33 if (!isKnownRuntimeId(runtimeId)) {
34 throw new Error(`Unsupported runtime: ${runtimeId}`);34 throw new Error(`Unsupported runtime: ${runtimeId}`);
35 }35 }
36 if (!isRuntimeEnabled(runtimeId)) {
37 throw new Error(`Runtime is disabled for this deployment: ${runtimeId}`);
38 }
36 39
37 const taskId = `task_${crypto.randomUUID()}`;40 const taskId = `task_${crypto.randomUUID()}`;
38 const task = (await ctx.runMutation(internal.database.createTask, {41 const task = (await ctx.runMutation(internal.database.createTask, {
@@ -212,7 +215,6 @@ export const completeRuntimeRun = internalMutation({ · 173 unmodified lines
212 args: {215 args: {
213 runId: v.string(),216 runId: v.string(),
214 status: v.union(v.literal("completed"), v.literal("failed"), v.literal("timed_out"), v.literal("denied")),217 status: v.union(v.literal("completed"), v.literal("failed"), v.literal("timed_out"), v.literal("denied")),
215 result: v.optional(v.any()),
216 exitCode: v.optional(v.number()),218 exitCode: v.optional(v.number()),
217 error: v.optional(v.string()),219 error: v.optional(v.string()),
218 durationMs: v.optional(v.number()),220 durationMs: v.optional(v.number()),
@@ -230,7 +232,6 @@ export const completeRuntimeRun = internalMutation({ · 11 unmodified lines
230 const finished = await markTaskFinished(ctx, {232 const finished = await markTaskFinished(ctx, {
231 taskId: args.runId,233 taskId: args.runId,
232 status: args.status,234 status: args.status,
233 result: args.result,
234 exitCode: args.exitCode,235 exitCode: args.exitCode,
235 error: args.error,236 error: args.error,
236 });237 });
executor/packages/convex/http/mcp_service.ts+2−1
@@ -4,6 +4,7 @@ import type { Id } from "../_generated/dataModel"; · 3 unmodified lines
4import type {4import type {
5 AnonymousContext,5 AnonymousContext,
6 PendingApprovalRecord,6 PendingApprovalRecord,
7 TaskExecutionOutcome,
7 TaskRecord,8 TaskRecord,
8 ToolDescriptor,9 ToolDescriptor,
9} from "../../core/src/types";10} from "../../core/src/types";
@@ -25,7 +26,7 @@ export function createMcpExecutorService(ctx: ActionCtx) { · 15 unmodified lines
25 };26 };
26 return await ctx.runMutation(internal.executor.createTaskInternal, taskInput);27 return await ctx.runMutation(internal.executor.createTaskInternal, taskInput);
27 },28 },
28 runTaskNow: async (taskId: string): Promise<null> => {29 runTaskNow: async (taskId: string): Promise<TaskExecutionOutcome | null> => {
29 return await ctx.runAction(internal.executorNode.runTask, { taskId });30 return await ctx.runAction(internal.executorNode.runTask, { taskId });
30 },31 },
31 getTask: async (taskId: string, workspaceId?: Id<"workspaces">): Promise<TaskRecord | null> => {32 getTask: async (taskId: string, workspaceId?: Id<"workspaces">): Promise<TaskRecord | null> => {
executor/packages/convex/runtime/task_runner.ts+28−13
@@ -9,9 +9,10 @@ import { · 8 unmodified lines
9 CLOUDFLARE_WORKER_LOADER_RUNTIME_ID,9 CLOUDFLARE_WORKER_LOADER_RUNTIME_ID,
10 isCloudflareWorkerLoaderConfigured,10 isCloudflareWorkerLoaderConfigured,
11 isKnownRuntimeId,11 isKnownRuntimeId,
12 isRuntimeEnabled,
12} from "../../core/src/runtimes/runtime-catalog";13} from "../../core/src/runtimes/runtime-catalog";
13import { runCodeWithAdapter } from "../../core/src/runtimes/runtime-core";14import { runCodeWithAdapter } from "../../core/src/runtimes/runtime-core";
14import type { TaskRecord } from "../../core/src/types";15import type { TaskExecutionOutcome, TaskRecord } from "../../core/src/types";
15import { describeError } from "../../core/src/utils";16import { describeError } from "../../core/src/utils";
16import { publishTaskEvent } from "./events";17import { publishTaskEvent } from "./events";
17import { taskTerminalEventType } from "../task/status";18import { taskTerminalEventType } from "../task/status";
@@ -21,7 +22,7 @@ import { invokeTool } from "./tool_invocation"; · 3 unmodified lines
21async function markTaskFailedAndPublish(22async function markTaskFailedAndPublish(
22 ctx: ActionCtx,23 ctx: ActionCtx,
23 args: { taskId: string; error: string },24 args: { taskId: string; error: string },
24): Promise<void> {25): Promise<TaskExecutionOutcome | null> {
25 const failed = await markTaskFinished(ctx, {26 const failed = await markTaskFinished(ctx, {
26 taskId: args.taskId,27 taskId: args.taskId,
27 status: "failed",28 status: "failed",
@@ -29,7 +30,7 @@ async function markTaskFailedAndPublish( · 1 unmodified line
29 });30 });
30 31
31 if (!failed) {32 if (!failed) {
32 return;33 return null;
33 }34 }
34 35
35 await publishTaskEvent(ctx, args.taskId, "task", "task.failed", {36 await publishTaskEvent(ctx, args.taskId, "task", "task.failed", {
@@ -38,6 +39,8 @@ async function markTaskFailedAndPublish( · 2 unmodified lines
38 error: failed.error,39 error: failed.error,
39 ...(failed.completedAt ? { completedAt: failed.completedAt } : {}),40 ...(failed.completedAt ? { completedAt: failed.completedAt } : {}),
40 });41 });
42
43 return { task: failed };
41}44}
42 45
43async function publishTerminalTaskResult(46async function publishTerminalTaskResult(
@@ -62,26 +65,31 @@ async function publishTerminalTaskResult( · 18 unmodified lines
62export async function runQueuedTask(65export async function runQueuedTask(
63 ctx: ActionCtx,66 ctx: ActionCtx,
64 args: { taskId: string },67 args: { taskId: string },
65): Promise<null> {68): Promise<TaskExecutionOutcome | null> {
66 const task = (await ctx.runQuery(internal.database.getTask, { taskId: args.taskId })) as TaskRecord | null;69 const task = (await ctx.runQuery(internal.database.getTask, { taskId: args.taskId })) as TaskRecord | null;
67 if (!task || task.status !== "queued") {70 if (!task || task.status !== "queued") {
68 return null;71 return null;
69 }72 }
70 73
71 if (!isKnownRuntimeId(task.runtimeId)) {74 if (!isKnownRuntimeId(task.runtimeId)) {
72 await markTaskFailedAndPublish(ctx, {75 return await markTaskFailedAndPublish(ctx, {
73 taskId: args.taskId,76 taskId: args.taskId,
74 error: `Runtime not found: ${task.runtimeId}`,77 error: `Runtime not found: ${task.runtimeId}`,
75 });78 });
76 return null;79 }
80
81 if (!isRuntimeEnabled(task.runtimeId)) {
82 return await markTaskFailedAndPublish(ctx, {
83 taskId: args.taskId,
84 error: `Runtime is disabled for this deployment: ${task.runtimeId}`,
85 });
77 }86 }
78 87
79 if (task.runtimeId === CLOUDFLARE_WORKER_LOADER_RUNTIME_ID && !isCloudflareWorkerLoaderConfigured()) {88 if (task.runtimeId === CLOUDFLARE_WORKER_LOADER_RUNTIME_ID && !isCloudflareWorkerLoaderConfigured()) {
80 await markTaskFailedAndPublish(ctx, {89 return await markTaskFailedAndPublish(ctx, {
81 taskId: args.taskId,90 taskId: args.taskId,
82 error: `Runtime is not configured: ${task.runtimeId}`,91 error: `Runtime is not configured: ${task.runtimeId}`,
83 });92 });
84 return null;
85 }93 }
86 94
87 try {95 try {
@@ -106,17 +114,15 @@ export async function runQueuedTask( · 18 unmodified lines
106 });114 });
107 115
108 if (!dispatchResult.ok) {116 if (!dispatchResult.ok) {
109 await markTaskFailedAndPublish(ctx, {117 return await markTaskFailedAndPublish(ctx, {
110 taskId: args.taskId,118 taskId: args.taskId,
111 error: dispatchResult.error,119 error: dispatchResult.error,
112 });120 });
113 return null;
114 }121 }
115 122
116 const finished = await markTaskFinished(ctx, {123 const finished = await markTaskFinished(ctx, {
117 taskId: args.taskId,124 taskId: args.taskId,
118 status: dispatchResult.status,125 status: dispatchResult.status,
119 result: dispatchResult.result,
120 exitCode: dispatchResult.exitCode,126 exitCode: dispatchResult.exitCode,
121 error: dispatchResult.error,127 error: dispatchResult.error,
122 });128 });
@@ -131,7 +137,11 @@ export async function runQueuedTask( · 8 unmodified lines
131 finished,137 finished,
132 durationMs: dispatchResult.durationMs,138 durationMs: dispatchResult.durationMs,
133 });139 });
134 return null;140 return {
141 task: finished,
142 result: dispatchResult.result,
143 durationMs: dispatchResult.durationMs,
144 };
135 }145 }
136 146
137 const runtimeResult = await (async () => {147 const runtimeResult = await (async () => {
@@ -153,7 +163,6 @@ export async function runQueuedTask( · 15 unmodified lines
153 const finished = await markTaskFinished(ctx, {163 const finished = await markTaskFinished(ctx, {
154 taskId: args.taskId,164 taskId: args.taskId,
155 status: runtimeResult.status,165 status: runtimeResult.status,
156 result: runtimeResult.result,
157 exitCode: runtimeResult.exitCode,166 exitCode: runtimeResult.exitCode,
158 error: runtimeResult.error,167 error: runtimeResult.error,
159 });168 });
@@ -168,6 +177,11 @@ export async function runQueuedTask( · 8 unmodified lines
168 finished,177 finished,
169 durationMs: runtimeResult.durationMs,178 durationMs: runtimeResult.durationMs,
170 });179 });
180 return {
181 task: finished,
182 result: runtimeResult.result,
183 durationMs: runtimeResult.durationMs,
184 };
171 } catch (error) {185 } catch (error) {
172 const message = describeError(error);186 const message = describeError(error);
173 const denied = message.startsWith(APPROVAL_DENIED_PREFIX);187 const denied = message.startsWith(APPROVAL_DENIED_PREFIX);
@@ -183,6 +197,7 @@ export async function runQueuedTask( · 9 unmodified lines
183 status: denied ? "denied" : "failed",197 status: denied ? "denied" : "failed",
184 finished,198 finished,
185 });199 });
200 return { task: finished };
186 }201 }
187 }202 }
188 203
executor/packages/convex/runtime/workspace_tools.ts+1−2
@@ -11,7 +11,6 @@ import { · 10 unmodified lines
11 type WorkspaceToolSnapshot,11 type WorkspaceToolSnapshot,
12} from "../../core/src/tool-sources";12} from "../../core/src/tool-sources";
13import type { ExternalToolSourceConfig } from "../../core/src/tool-source-types";13import type { ExternalToolSourceConfig } from "../../core/src/tool-source-types";
14import { DEFAULT_TOOLS } from "../../core/src/tools";
15import type {14import type {
16 AccessPolicyRecord,15 AccessPolicyRecord,
17 OpenApiSourceQuality,16 OpenApiSourceQuality,
@@ -22,7 +21,7 @@ import type { · 4 unmodified lines
22import { computeOpenApiSourceQuality, listVisibleToolDescriptors } from "./tool_descriptors";21import { computeOpenApiSourceQuality, listVisibleToolDescriptors } from "./tool_descriptors";
23import { loadSourceArtifact, normalizeExternalToolSource, sourceSignature } from "./tool_source_loading";22import { loadSourceArtifact, normalizeExternalToolSource, sourceSignature } from "./tool_source_loading";
24 23
25const baseTools = new Map<string, ToolDefinition>(DEFAULT_TOOLS.map((tool) => [tool.path, tool]));24const baseTools = new Map<string, ToolDefinition>();
26 25
27export interface DtsStorageEntry {26export interface DtsStorageEntry {
28 sourceKey: string;27 sourceKey: string;
executor/packages/convex/runtimeCallbacks.ts+0−2
@@ -37,7 +37,6 @@ export const completeRun = mutation({ · 36 unmodified lines
37 internalSecret: v.string(),37 internalSecret: v.string(),
38 runId: v.string(),38 runId: v.string(),
39 status: v.union(v.literal("completed"), v.literal("failed"), v.literal("timed_out"), v.literal("denied")),39 status: v.union(v.literal("completed"), v.literal("failed"), v.literal("timed_out"), v.literal("denied")),
40 result: v.optional(v.any()),
41 exitCode: v.optional(v.number()),40 exitCode: v.optional(v.number()),
42 error: v.optional(v.string()),41 error: v.optional(v.string()),
43 durationMs: v.optional(v.number()),42 durationMs: v.optional(v.number()),
@@ -48,7 +47,6 @@ export const completeRun = mutation({ · 4 unmodified lines
48 return await ctx.runMutation(internal.executor.completeRuntimeRun, {47 return await ctx.runMutation(internal.executor.completeRuntimeRun, {
49 runId: args.runId,48 runId: args.runId,
50 status: args.status,49 status: args.status,
51 result: args.result,
52 exitCode: args.exitCode,50 exitCode: args.exitCode,
53 error: args.error,51 error: args.error,
54 durationMs: args.durationMs,52 durationMs: args.durationMs,
executor/packages/convex/schema.ts+1−2
@@ -53,7 +53,7 @@ export default defineSchema({ · 52 unmodified lines
53 accounts: defineTable({53 accounts: defineTable({
54 provider: accountProvider,54 provider: accountProvider,
55 providerAccountId: v.string(), // WorkOS user ID or anon_* UUID55 providerAccountId: v.string(), // WorkOS user ID or anon_* UUID
56 email: v.string(),56 email: v.optional(v.string()),
57 name: v.string(),57 name: v.string(),
58 firstName: v.optional(v.string()),58 firstName: v.optional(v.string()),
59 lastName: v.optional(v.string()),59 lastName: v.optional(v.string()),
@@ -188,7 +188,6 @@ export default defineSchema({ · 128 unmodified lines
188 error: v.optional(v.string()),188 error: v.optional(v.string()),
189 stdout: v.optional(v.string()),189 stdout: v.optional(v.string()),
190 stderr: v.optional(v.string()),190 stderr: v.optional(v.string()),
191 result: v.optional(v.any()),
192 exitCode: v.optional(v.number()),191 exitCode: v.optional(v.number()),
193 createdAt: v.number(),192 createdAt: v.number(),
194 updatedAt: v.number(),193 updatedAt: v.number(),
executor/packages/convex/task/finish.ts+0−1
@@ -8,7 +8,6 @@ type TaskFinishContext = Pick<ActionCtx, "runMutation"> | Pick<MutationCtx, "run · 7 unmodified lines
8export interface MarkTaskFinishedInput {8export interface MarkTaskFinishedInput {
9 taskId: string;9 taskId: string;
10 status: TerminalTaskStatus;10 status: TerminalTaskStatus;
11 result?: unknown;
12 exitCode?: number;11 exitCode?: number;
13 error?: string;12 error?: string;
14}13}
executor/packages/core/src/mcp-server-contracts.ts+2−1
@@ -3,6 +3,7 @@ import type { · 2 unmodified lines
3 AnonymousContext,3 AnonymousContext,
4 CreateTaskInput,4 CreateTaskInput,
5 PendingApprovalRecord,5 PendingApprovalRecord,
6 TaskExecutionOutcome,
6 TaskRecord,7 TaskRecord,
7 ToolDescriptor,8 ToolDescriptor,
8} from "./types";9} from "./types";
@@ -10,7 +11,7 @@ import type { Id } from "../../convex/_generated/dataModel"; · 1 unmodified line
10 11
11export interface McpExecutorService {12export interface McpExecutorService {
12 createTask(input: CreateTaskInput): Promise<{ task: TaskRecord }>;13 createTask(input: CreateTaskInput): Promise<{ task: TaskRecord }>;
13 runTaskNow?(taskId: string): Promise<null>;14 runTaskNow?(taskId: string): Promise<TaskExecutionOutcome | null>;
14 getTask(taskId: string, workspaceId?: Id<"workspaces">): Promise<TaskRecord | null>;15 getTask(taskId: string, workspaceId?: Id<"workspaces">): Promise<TaskRecord | null>;
15 subscribe(taskId: string, workspaceId: Id<"workspaces">, listener: (event: LiveTaskEvent) => void): () => void;16 subscribe(taskId: string, workspaceId: Id<"workspaces">, listener: (event: LiveTaskEvent) => void): () => void;
16 bootstrapAnonymousContext(sessionId?: string): Promise<AnonymousContext>;17 bootstrapAnonymousContext(sessionId?: string): Promise<AnonymousContext>;
executor/packages/core/src/mcp-server-formatting.ts+3−3
@@ -15,7 +15,7 @@ function listTopLevelToolKeys(tools: ToolDescriptor[]): string[] { · 14 unmodified lines
15 return [...keys].sort();15 return [...keys].sort();
16}16}
17 17
18export function summarizeTask(task: TaskRecord): string {18export function summarizeTask(task: TaskRecord, result: unknown = task.result): string {
19 const maxResultPreviewChars = 30_000;19 const maxResultPreviewChars = 30_000;
20 const lines = [20 const lines = [
21 `taskId: ${task.id}`,21 `taskId: ${task.id}`,
@@ -32,8 +32,8 @@ export function summarizeTask(task: TaskRecord): string { · 10 unmodified lines
32 }32 }
33 33
34 let text = lines.join("\n");34 let text = lines.join("\n");
35 if (task.result !== undefined) {35 if (result !== undefined) {
36 const serialized = Result.try(() => JSON.stringify(task.result, null, 2)).unwrapOr(String(task.result));36 const serialized = Result.try(() => JSON.stringify(result, null, 2)).unwrapOr(String(result));
37 if (serialized.length > maxResultPreviewChars) {37 if (serialized.length > maxResultPreviewChars) {
38 text += asCodeBlock(38 text += asCodeBlock(
39 "json",39 "json",
executor/packages/core/src/mcp-server.ts+5−4
@@ -87,8 +87,8 @@ function createRunCodeTool( · 86 unmodified lines
87 }87 }
88 88
89 if (service.runTaskNow) {89 if (service.runTaskNow) {
90 await service.runTaskNow(created.task.id);90 const runOutcome = await service.runTaskNow(created.task.id);
91 const task = await service.getTask(created.task.id, context.workspaceId);91 const task = runOutcome?.task ?? await service.getTask(created.task.id, context.workspaceId);
92 if (!task) {92 if (!task) {
93 return {93 return {
94 content: [textContent(`Task ${created.task.id} not found after execution`)],94 content: [textContent(`Task ${created.task.id} not found after execution`)],
@@ -96,16 +96,17 @@ function createRunCodeTool( · 1 unmodified line
96 };96 };
97 }97 }
98 98
99 const result = runOutcome?.result ?? task.result;
99 const isError = task.status !== "completed";100 const isError = task.status !== "completed";
100 return {101 return {
101 content: [textContent(summarizeTask(task))],102 content: [textContent(summarizeTask(task, result))],
102 structuredContent: {103 structuredContent: {
103 taskId: task.id,104 taskId: task.id,
104 status: task.status,105 status: task.status,
105 runtimeId: task.runtimeId,106 runtimeId: task.runtimeId,
106 exitCode: task.exitCode,107 exitCode: task.exitCode,
107 error: task.error,108 error: task.error,
108 result: task.result,109 result,
109 workspaceId: context.workspaceId,110 workspaceId: context.workspaceId,
110 actorId: context.actorId,111 actorId: context.actorId,
111 sessionId: context.sessionId,112 sessionId: context.sessionId,
executor/packages/core/src/runtimes/cloudflare-worker-loader-runtime.test.ts+29−0
@@ -153,6 +153,35 @@ describe("runtime catalog", () => { · 152 unmodified lines
153 expect(isCloudflareWorkerLoaderConfigured()).toBe(true);153 expect(isCloudflareWorkerLoaderConfigured()).toBe(true);
154 });154 });
155 155
156 test("can restrict runtime targets to cloudflare dynamic worker", async () => {
157 const {
158 CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY,
159 CLOUDFLARE_WORKER_LOADER_RUNTIME_ID,
160 LOCAL_BUN_RUNTIME_ID,
161 defaultRuntimeId,
162 isRuntimeEnabled,
163 listRuntimeTargets,
164 } = await import("./runtime-catalog");
165
166 const previous = process.env[CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY];
167 try {
168 process.env[CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY] = "1";
169
170 expect(defaultRuntimeId()).toBe(CLOUDFLARE_WORKER_LOADER_RUNTIME_ID);
171 expect(isRuntimeEnabled(LOCAL_BUN_RUNTIME_ID)).toBe(false);
172 expect(isRuntimeEnabled(CLOUDFLARE_WORKER_LOADER_RUNTIME_ID)).toBe(true);
173 expect(listRuntimeTargets().map((target) => target.id)).toEqual([
174 CLOUDFLARE_WORKER_LOADER_RUNTIME_ID,
175 ]);
176 } finally {
177 if (previous === undefined) {
178 delete process.env[CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY];
179 } else {
180 process.env[CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY] = previous;
181 }
182 }
183 });
184
156 test("getCloudflareWorkerLoaderConfig reads env vars", async () => {185 test("getCloudflareWorkerLoaderConfig reads env vars", async () => {
157 const { getCloudflareWorkerLoaderConfig } = await import("./runtime-catalog");186 const { getCloudflareWorkerLoaderConfig } = await import("./runtime-catalog");
158 const config = getCloudflareWorkerLoaderConfig();187 const config = getCloudflareWorkerLoaderConfig();
executor/packages/core/src/runtimes/runtime-catalog.ts+53−0
@@ -2,6 +2,9 @@ · 1 unmodified line
2 2
3export const LOCAL_BUN_RUNTIME_ID = "local-bun";3export const LOCAL_BUN_RUNTIME_ID = "local-bun";
4export const CLOUDFLARE_WORKER_LOADER_RUNTIME_ID = "cloudflare-worker-loader";4export const CLOUDFLARE_WORKER_LOADER_RUNTIME_ID = "cloudflare-worker-loader";
5export const CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY = "EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY";
6
7const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
5 8
6const KNOWN_RUNTIME_IDS = new Set([9const KNOWN_RUNTIME_IDS = new Set([
7 LOCAL_BUN_RUNTIME_ID,10 LOCAL_BUN_RUNTIME_ID,
@@ -12,6 +15,56 @@ export function isKnownRuntimeId(runtimeId: string): boolean { · 4 unmodified lines
12 return KNOWN_RUNTIME_IDS.has(runtimeId);15 return KNOWN_RUNTIME_IDS.has(runtimeId);
13}16}
14 17
18function isTruthyEnvValue(value: string | undefined): boolean {
19 if (!value) {
20 return false;
21 }
22 return TRUTHY_ENV_VALUES.has(value.trim().toLowerCase());
23}
24
25export function isCloudflareDynamicWorkerOnlyMode(): boolean {
26 return isTruthyEnvValue(process.env[CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY]);
27}
28
29export function isRuntimeEnabled(runtimeId: string): boolean {
30 if (!isKnownRuntimeId(runtimeId)) {
31 return false;
32 }
33 if (!isCloudflareDynamicWorkerOnlyMode()) {
34 return true;
35 }
36 return runtimeId === CLOUDFLARE_WORKER_LOADER_RUNTIME_ID;
37}
38
39export function defaultRuntimeId(): string {
40 return isCloudflareDynamicWorkerOnlyMode()
41 ? CLOUDFLARE_WORKER_LOADER_RUNTIME_ID
42 : LOCAL_BUN_RUNTIME_ID;
43}
44
45export interface RuntimeTargetDescriptor {
46 id: string;
47 label: string;
48 description: string;
49}
50
51const RUNTIME_TARGETS: RuntimeTargetDescriptor[] = [
52 {
53 id: LOCAL_BUN_RUNTIME_ID,
54 label: "Local JS Runtime",
55 description: "Runs generated code in-process using Bun",
56 },
57 {
58 id: CLOUDFLARE_WORKER_LOADER_RUNTIME_ID,
59 label: "Cloudflare Worker Loader",
60 description: "Runs generated code in a Cloudflare Worker",
61 },
62];
63
64export function listRuntimeTargets(): RuntimeTargetDescriptor[] {
65 return RUNTIME_TARGETS.filter((target) => isRuntimeEnabled(target.id));
66}
67
15// ── Cloudflare Worker Loader config ──────────────────────────────────────────68// ── Cloudflare Worker Loader config ──────────────────────────────────────────
16 69
17export interface CloudflareWorkerLoaderConfig {70export interface CloudflareWorkerLoaderConfig {
executor/packages/core/src/tools.ts+0−82
@@ -1,82 +0,0 @@
1import type { ToolDefinition } from "./types";
2import { asRecord } from "./utils";
3
4export const DEFAULT_TOOLS: ToolDefinition[] = [
5 {
6 path: "utils.get_time",
7 description: "Return current server time.",
8 approval: "auto",
9 source: "local",
10 metadata: {
11 argsType: "{}",
12 returnsType: "{ iso: string; unix: number }",
13 },
14 run: async () => ({
15 iso: new Date().toISOString(),
16 unix: Date.now(),
17 }),
18 },
19 {
20 path: "math.add",
21 description: "Add two numbers.",
22 approval: "auto",
23 source: "local",
24 metadata: {
25 argsType: "{ a: number; b: number }",
26 returnsType: "{ result: number }",
27 },
28 run: async (input) => {
29 const payload = asRecord(input);
30 const a = Number(payload.a ?? 0);
31 const b = Number(payload.b ?? 0);
32 if (!Number.isFinite(a) || !Number.isFinite(b)) {
33 throw new Error("math.add expects numeric a and b");
34 }
35 return { result: a + b };
36 },
37 },
38 {
39 path: "admin.send_announcement",
40 description: "Mock announcement sender that requires approval.",
41 approval: "required",
42 source: "local",
43 metadata: {
44 argsType: "{ channel?: string; message: string }",
45 returnsType: "{ sent: true; channel: string; message: string }",
46 },
47 run: async (input) => {
48 const payload = asRecord(input);
49 const channel = String(payload.channel ?? "general");
50 const message = String(payload.message ?? "");
51 if (!message.trim()) {
52 throw new Error("admin.send_announcement requires a non-empty message");
53 }
54 return {
55 sent: true,
56 channel,
57 message,
58 };
59 },
60 },
61 {
62 path: "admin.delete_data",
63 description: "Mock destructive operation that requires approval.",
64 approval: "required",
65 source: "local",
66 metadata: {
67 argsType: "{ key: string }",
68 returnsType: "{ deleted: true; key: string }",
69 },
70 run: async (input) => {
71 const payload = asRecord(input);
72 const key = String(payload.key ?? "");
73 if (!key.trim()) {
74 throw new Error("admin.delete_data requires key");
75 }
76 return {
77 deleted: true,
78 key,
79 };
80 },
81 },
82];
executor/packages/core/src/types.ts+6−0
@@ -39,6 +39,12 @@ export interface TaskRecord { · 38 unmodified lines
39 exitCode?: number;39 exitCode?: number;
40}40}
41 41
42export interface TaskExecutionOutcome {
43 task: TaskRecord;
44 result?: unknown;
45 durationMs?: number;
46}
47
42export interface ApprovalRecord {48export interface ApprovalRecord {
43 id: string;49 id: string;
44 taskId: string;50 taskId: string;
executor/scripts/prod/doctor-prod.test.ts+26−0
@@ -24,6 +24,7 @@ describe("buildEnvChecks", () => { · 23 unmodified lines
24 expect(byName.get("env:CLOUDFLARE_SANDBOX_RUN_URL")?.ok).toBe(false);24 expect(byName.get("env:CLOUDFLARE_SANDBOX_RUN_URL")?.ok).toBe(false);
25 expect(byName.get("env:CLOUDFLARE_SANDBOX_AUTH_TOKEN")?.ok).toBe(false);25 expect(byName.get("env:CLOUDFLARE_SANDBOX_AUTH_TOKEN")?.ok).toBe(false);
26 expect(byName.get("env:EXECUTOR_INTERNAL_TOKEN")?.ok).toBe(false);26 expect(byName.get("env:EXECUTOR_INTERNAL_TOKEN")?.ok).toBe(false);
27 expect(byName.get("env:EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY")?.ok).toBe(false);
27 expect(byName.get("env:CONVEX_URL")?.ok).toBe(false);28 expect(byName.get("env:CONVEX_URL")?.ok).toBe(false);
28 expect(byName.get("env:WORKOS_CLIENT_ID")?.ok).toBe(false);29 expect(byName.get("env:WORKOS_CLIENT_ID")?.ok).toBe(false);
29 expect(byName.get("env:WORKOS_API_KEY")?.ok).toBe(false);30 expect(byName.get("env:WORKOS_API_KEY")?.ok).toBe(false);
@@ -40,6 +41,7 @@ describe("buildEnvChecks", () => { · 10 unmodified lines
40 ["CLOUDFLARE_SANDBOX_RUN_URL", "https://executor-sandbox-host.example.workers.dev/v1/runs"],41 ["CLOUDFLARE_SANDBOX_RUN_URL", "https://executor-sandbox-host.example.workers.dev/v1/runs"],
41 ["CLOUDFLARE_SANDBOX_AUTH_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],42 ["CLOUDFLARE_SANDBOX_AUTH_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],
42 ["EXECUTOR_INTERNAL_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],43 ["EXECUTOR_INTERNAL_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],
44 ["EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY", "1"],
43 ["CONVEX_URL", "https://perceptive-pigeon-577.convex.site"],45 ["CONVEX_URL", "https://perceptive-pigeon-577.convex.site"],
44 ["WORKOS_CLIENT_ID", "client_123"],46 ["WORKOS_CLIENT_ID", "client_123"],
45 ["WORKOS_API_KEY", "sk_test_123"],47 ["WORKOS_API_KEY", "sk_test_123"],
@@ -61,6 +63,7 @@ describe("buildEnvChecks", () => { · 15 unmodified lines
61 ["CLOUDFLARE_SANDBOX_RUN_URL", "https://executor-sandbox-host.example.workers.dev/v1/runs"],63 ["CLOUDFLARE_SANDBOX_RUN_URL", "https://executor-sandbox-host.example.workers.dev/v1/runs"],
62 ["CLOUDFLARE_SANDBOX_AUTH_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],64 ["CLOUDFLARE_SANDBOX_AUTH_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],
63 ["EXECUTOR_INTERNAL_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],65 ["EXECUTOR_INTERNAL_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],
66 ["EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY", "1"],
64 ["CONVEX_URL", "https://perceptive-pigeon-577.convex.cloud"],67 ["CONVEX_URL", "https://perceptive-pigeon-577.convex.cloud"],
65 ["CONVEX_SITE_URL", "https://perceptive-pigeon-577.convex.site"],68 ["CONVEX_SITE_URL", "https://perceptive-pigeon-577.convex.site"],
66 ["WORKOS_CLIENT_ID", "client_123"],69 ["WORKOS_CLIENT_ID", "client_123"],
@@ -77,4 +80,27 @@ describe("buildEnvChecks", () => { · 10 unmodified lines
77 const failures = checks.filter((check) => !check.ok);80 const failures = checks.filter((check) => !check.ok);
78 expect(failures).toEqual([]);81 expect(failures).toEqual([]);
79 });82 });
83
84 test("requires cloudflare-only runtime mode to be truthy", () => {
85 const env = new Map<string, string>([
86 ["CLOUDFLARE_SANDBOX_RUN_URL", "https://executor-sandbox-host.example.workers.dev/v1/runs"],
87 ["CLOUDFLARE_SANDBOX_AUTH_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],
88 ["EXECUTOR_INTERNAL_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"],
89 ["EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY", "0"],
90 ["CONVEX_URL", "https://perceptive-pigeon-577.convex.cloud"],
91 ["CONVEX_SITE_URL", "https://perceptive-pigeon-577.convex.site"],
92 ["WORKOS_CLIENT_ID", "client_123"],
93 ["WORKOS_API_KEY", "sk_test_123"],
94 ["WORKOS_WEBHOOK_SECRET", "whsec_123"],
95 ["WORKOS_COOKIE_PASSWORD", "cookie_password_123"],
96 ["STRIPE_SECRET_KEY", "sk_live_123"],
97 ["STRIPE_WEBHOOK_SECRET", "whsec_live_123"],
98 ["STRIPE_PRICE_ID", "price_123"],
99 ["MCP_AUTHORIZATION_SERVER", "https://auth.example.com"],
100 ]);
101
102 const checks = buildEnvChecks(env);
103 const runtimeModeCheck = checks.find((check) => check.name === "runtime restriction mode");
104 expect(runtimeModeCheck?.ok).toBe(false);
105 });
80});106});
executor/scripts/prod/doctor-prod.ts+28−0
@@ -20,6 +20,7 @@ const REQUIRED_PROD_ENV_KEYS = [ · 19 unmodified lines
20 "CLOUDFLARE_SANDBOX_RUN_URL",20 "CLOUDFLARE_SANDBOX_RUN_URL",
21 "CLOUDFLARE_SANDBOX_AUTH_TOKEN",21 "CLOUDFLARE_SANDBOX_AUTH_TOKEN",
22 "EXECUTOR_INTERNAL_TOKEN",22 "EXECUTOR_INTERNAL_TOKEN",
23 "EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY",
23 "CONVEX_URL",24 "CONVEX_URL",
24] as const;25] as const;
25 26
@@ -113,6 +114,13 @@ function parseUrl(input: string): URL | null { · 87 unmodified lines
113 }114 }
114}115}
115 116
117function isTruthy(value: string | undefined): boolean {
118 if (!value) {
119 return false;
120 }
121 return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
122}
123
116function deploymentPrefix(hostname: string, suffix: ".convex.cloud" | ".convex.site"): string | null {124function deploymentPrefix(hostname: string, suffix: ".convex.cloud" | ".convex.site"): string | null {
117 if (!hostname.endsWith(suffix)) {125 if (!hostname.endsWith(suffix)) {
118 return null;126 return null;
@@ -254,6 +262,19 @@ export function buildEnvChecks(env: Map<string, string>): DoctorCheck[] { · 135 unmodified lines
254 addTokenStrengthCheck(report, "EXECUTOR_INTERNAL_TOKEN", internalSecret);262 addTokenStrengthCheck(report, "EXECUTOR_INTERNAL_TOKEN", internalSecret);
255 }263 }
256 264
265 const cloudflareOnlyMode = env.get("EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY")?.trim();
266 if (cloudflareOnlyMode) {
267 const enabled = isTruthy(cloudflareOnlyMode);
268 addCheck(
269 report,
270 "runtime restriction mode",
271 enabled,
272 enabled
273 ? "Cloudflare dynamic worker runtime-only mode is enabled"
274 : `expected truthy value, got '${cloudflareOnlyMode}'`,
275 );
276 }
277
257 for (const key of OPTIONAL_BILLING_ENV_KEYS) {278 for (const key of OPTIONAL_BILLING_ENV_KEYS) {
258 const value = env.get(key)?.trim();279 const value = env.get(key)?.trim();
259 addCheck(280 addCheck(
@@ -272,12 +293,19 @@ function guidanceForFailure(checkName: string): string[] { · 12 unmodified lines
272 checkName === "env:CLOUDFLARE_SANDBOX_RUN_URL"293 checkName === "env:CLOUDFLARE_SANDBOX_RUN_URL"
273 || checkName === "env:CLOUDFLARE_SANDBOX_AUTH_TOKEN"294 || checkName === "env:CLOUDFLARE_SANDBOX_AUTH_TOKEN"
274 || checkName === "env:EXECUTOR_INTERNAL_TOKEN"295 || checkName === "env:EXECUTOR_INTERNAL_TOKEN"
296 || checkName === "env:EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY"
275 ) {297 ) {
276 return [298 return [
277 "Run `bun run setup:prod:cloudflare --deploy` (or `bun run setup:prod:all`) to deploy sandbox host and set required Cloudflare runtime env vars.",299 "Run `bun run setup:prod:cloudflare --deploy` (or `bun run setup:prod:all`) to deploy sandbox host and set required Cloudflare runtime env vars.",
278 ];300 ];
279 }301 }
280 302
303 if (checkName === "runtime restriction mode") {
304 return [
305 "Set `EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY=1` in Convex prod env to force Cloudflare dynamic worker runtime.",
306 ];
307 }
308
281 if (checkName === "env:CONVEX_URL") {309 if (checkName === "env:CONVEX_URL") {
282 return [310 return [
283 "Set Convex API URL: `bunx convex env set CONVEX_URL https://<deployment>.convex.cloud --prod`.",311 "Set Convex API URL: `bunx convex env set CONVEX_URL https://<deployment>.convex.cloud --prod`.",
executor/scripts/prod/setup-prod-cloudflare.ts+2−0
@@ -260,12 +260,14 @@ async function main(): Promise<void> { · 259 unmodified lines
260 await setConvexEnv("CLOUDFLARE_SANDBOX_RUN_URL", runUrl);260 await setConvexEnv("CLOUDFLARE_SANDBOX_RUN_URL", runUrl);
261 await setConvexEnv("CLOUDFLARE_SANDBOX_AUTH_TOKEN", authToken);261 await setConvexEnv("CLOUDFLARE_SANDBOX_AUTH_TOKEN", authToken);
262 await setConvexEnv("EXECUTOR_INTERNAL_TOKEN", internalToken);262 await setConvexEnv("EXECUTOR_INTERNAL_TOKEN", internalToken);
263 await setConvexEnv("EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY", "1");
263 await setConvexEnv("CONVEX_URL", convexUrl);264 await setConvexEnv("CONVEX_URL", convexUrl);
264 265
265 console.log("\n[setup] cloudflare production runtime configured");266 console.log("\n[setup] cloudflare production runtime configured");
266 console.log(` CLOUDFLARE_SANDBOX_RUN_URL: ${runUrl}`);267 console.log(` CLOUDFLARE_SANDBOX_RUN_URL: ${runUrl}`);
267 console.log(` CLOUDFLARE_SANDBOX_AUTH_TOKEN: ${redact(authToken)}`);268 console.log(` CLOUDFLARE_SANDBOX_AUTH_TOKEN: ${redact(authToken)}`);
268 console.log(` EXECUTOR_INTERNAL_TOKEN: ${redact(internalToken)}`);269 console.log(` EXECUTOR_INTERNAL_TOKEN: ${redact(internalToken)}`);
270 console.log(" EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY: 1");
269 console.log(` CONVEX_URL: ${convexUrl}`);271 console.log(` CONVEX_URL: ${convexUrl}`);
270 272
271 if (options.runDoctor) {273 if (options.runDoctor) {