- 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.
Unified · Split
| @@ -7,6 +7,10 @@ CONVEX_DEPLOYMENT=dev:your-deployment-slug · 6 unmodified lines | ||
| 7 | 7 | CONVEX_URL=https://your-deployment.convex.cloud |
| 8 | 8 | CONVEX_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 | 14 | # ── WorkOS (optional — enables auth, omit for guest-only mode) ── |
| 11 | 15 | # WORKOS_CLIENT_ID=client_... |
| 12 | 16 | # WORKOS_API_KEY=sk_test_... |
| @@ -136,6 +136,7 @@ Important env vars (see root `.env.example` for the base template): · 135 unmodified lines | ||
| 136 | 136 | - Core: |
| 137 | 137 | - `CONVEX_URL` |
| 138 | 138 | - `CONVEX_SITE_URL` |
| 139 | + - `EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY` (`1` in production to force Cloudflare dynamic worker runtime) | |
| 139 | 140 | - WorkOS (optional auth/org features): |
| 140 | 141 | - `WORKOS_CLIENT_ID` |
| 141 | 142 | - `WORKOS_API_KEY` |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { StaticAppShell } from "@/components/static-app-shell"; |
| 2 | 2 | |
| 3 | −export default function ApprovalsPage() { | |
| 3 | +export default function DashboardRoutesPage() { | |
| 4 | 4 | return <StaticAppShell />; |
| 5 | 5 | } |
| @@ -1,5 +0,0 @@ | ||
| 1 | −import { redirect } from "next/navigation"; | |
| 2 | − | |
| 3 | −export default function BillingPage() { | |
| 4 | − redirect("/organization?tab=billing"); | |
| 5 | −} | |
| @@ -1,5 +0,0 @@ | ||
| 1 | −import { redirect } from "next/navigation"; | |
| 2 | − | |
| 3 | −export default function MembersPage() { | |
| 4 | − redirect("/organization?tab=members"); | |
| 5 | −} | |
| @@ -1,5 +0,0 @@ | ||
| 1 | −import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | − | |
| 3 | −export default function OnboardingPage() { | |
| 4 | − return <StaticAppShell />; | |
| 5 | −} | |
| @@ -1,5 +0,0 @@ | ||
| 1 | −import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | − | |
| 3 | −export default function OrganizationPage() { | |
| 4 | − return <StaticAppShell />; | |
| 5 | −} | |
| @@ -1,5 +0,0 @@ | ||
| 1 | −import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | − | |
| 3 | −export default function DashboardPage() { | |
| 4 | − return <StaticAppShell />; | |
| 5 | −} | |
| @@ -1,5 +0,0 @@ | ||
| 1 | −import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | − | |
| 3 | −export default function StaticAppShellPage() { | |
| 4 | − return <StaticAppShell />; | |
| 5 | −} | |
| @@ -1,5 +0,0 @@ | ||
| 1 | −import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | − | |
| 3 | −export default function TasksPage() { | |
| 4 | − return <StaticAppShell />; | |
| 5 | −} | |
| @@ -1,5 +0,0 @@ | ||
| 1 | −import { StaticAppShell } from "@/components/static-app-shell"; | |
| 2 | − | |
| 3 | −export default function ToolsPage() { | |
| 4 | − return <StaticAppShell />; | |
| 5 | −} | |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | −import { useState } from "react"; | |
| 3 | +import { useEffect, useState } from "react"; | |
| 4 | 4 | import { Play, Send } from "lucide-react"; |
| 5 | 5 | import { useMutation, useQuery } from "convex/react"; |
| 6 | 6 | import { toast } from "sonner"; |
| @@ -21,20 +21,12 @@ import { useSession } from "@/lib/session-context"; · 14 unmodified lines | ||
| 21 | 21 | import type { RuntimeTargetDescriptor } from "@/lib/types"; |
| 22 | 22 | import { useWorkspaceTools } from "@/hooks/use-workspace-tools"; |
| 23 | 23 | |
| 24 | −const DEFAULT_CODE = `// Example: call some tools and return a compact result | |
| 25 | −const time = await tools.utils.get_time(); | |
| 26 | −const sum = await tools.math.add({ a: 7, b: 35 }); | |
| 27 | − | |
| 28 | −// This will require approval: | |
| 29 | −await tools.admin.send_announcement({ | |
| 30 | − channel: "general", | |
| 31 | − message: "Hello from executor!" | |
| 24 | +const DEFAULT_CODE = `// Example: discover tools and return matching tool names | |
| 25 | +const found = await tools.discover({ | |
| 26 | + query: "Discover", | |
| 32 | 27 | }); |
| 33 | 28 | |
| 34 | −return { | |
| 35 | − isoTime: time.iso, | |
| 36 | − total: sum.result, | |
| 37 | −};`; | |
| 29 | +return found.results.map((tool) => tool.path);`; | |
| 38 | 30 | const DEFAULT_TIMEOUT_MS = 300_000; |
| 39 | 31 | |
| 40 | 32 | export function TaskComposer() { |
| @@ -47,14 +39,27 @@ export function TaskComposer() { · 6 unmodified lines | ||
| 47 | 39 | const runtimes = useQuery(convexApi.workspace.listRuntimeTargets, {}); |
| 48 | 40 | const createTask = useMutation(convexApi.executor.createTask); |
| 49 | 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 | 53 | const handleSubmit = async () => { |
| 52 | 54 | if (!context || !code.trim()) return; |
| 53 | 55 | setSubmitting(true); |
| 54 | 56 | try { |
| 57 | + const selectedRuntimeId = runtimeTargets.some((runtime: RuntimeTargetDescriptor) => runtime.id === runtimeId) | |
| 58 | + ? runtimeId | |
| 59 | + : undefined; | |
| 55 | 60 | const data = await createTask({ |
| 56 | 61 | code, |
| 57 | − runtimeId, | |
| 62 | + runtimeId: selectedRuntimeId, | |
| 58 | 63 | timeoutMs: Number.parseInt(timeoutMs, 10) || DEFAULT_TIMEOUT_MS, |
| 59 | 64 | workspaceId: context.workspaceId, |
| 60 | 65 | sessionId: context.sessionId, |
| @@ -87,7 +92,7 @@ export function TaskComposer() { · 26 unmodified lines | ||
| 87 | 92 | <SelectValue /> |
| 88 | 93 | </SelectTrigger> |
| 89 | 94 | <SelectContent> |
| 90 | − {(runtimes ?? []).map((r: RuntimeTargetDescriptor) => ( | |
| 95 | + {runtimeTargets.map((r: RuntimeTargetDescriptor) => ( | |
| 91 | 96 | <SelectItem key={r.id} value={r.id} className="text-xs"> |
| 92 | 97 | {r.label} |
| 93 | 98 | </SelectItem> |
| @@ -11,6 +11,8 @@ const codePlugin = createCodePlugin({ · 10 unmodified lines | ||
| 11 | 11 | |
| 12 | 12 | export function ToolDetail({ tool, depth }: { tool: ToolDescriptor; depth: number }) { |
| 13 | 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 | 17 | return ( |
| 16 | 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 | 39 | </div> |
| 38 | 40 | |
| 39 | − {tool.argsType && <TypeSignature raw={tool.argsType} label="Arguments" />} | |
| 40 | − {tool.returnsType && <TypeSignature raw={tool.returnsType} label="Returns" />} | |
| 41 | + {argsType && <TypeSignature raw={argsType} label="Arguments" />} | |
| 42 | + {returnsType && <TypeSignature raw={returnsType} label="Returns" />} | |
| 41 | 43 | </div> |
| 42 | 44 | ); |
| 43 | 45 | } |
| @@ -5,7 +5,7 @@ export interface WorkosAccount { · 4 unmodified lines | ||
| 5 | 5 | _id: Id<"accounts">; |
| 6 | 6 | provider: "workos" | "anonymous"; |
| 7 | 7 | providerAccountId: string; |
| 8 | − email: string; | |
| 8 | + email?: string; | |
| 9 | 9 | name: string; |
| 10 | 10 | avatarUrl?: string | null; |
| 11 | 11 | } |
| @@ -271,8 +271,26 @@ export function withUniqueSourceName(baseName: string, takenNames: Set<string>): · 270 unmodified lines | ||
| 271 | 271 | } |
| 272 | 272 | } |
| 273 | 273 | |
| 274 | −export function catalogSourceName(item: { providerName: string; name: string }): string { | |
| 274 | +function 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 | + | |
| 285 | +export 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 | 290 | const owner = sanitizeSourceName(item.providerName || "catalog"); |
| 276 | 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 | } |
| @@ -326,7 +326,6 @@ export declare const api: { · 325 unmodified lines | ||
| 326 | 326 | error?: string; |
| 327 | 327 | exitCode?: number; |
| 328 | 328 | internalSecret: string; |
| 329 | − result?: any; | |
| 330 | 329 | runId: string; |
| 331 | 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 | 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 | 613 | bootstrapAnonymousSession: FunctionReference< |
| 561 | 614 | "mutation", |
| 562 | 615 | "internal", |
| @@ -602,6 +655,41 @@ export declare const internal: { · 39 unmodified lines | ||
| 602 | 655 | { eventName: string; payload: any; taskId: string; type: string }, |
| 603 | 656 | 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 | 693 | deleteToolSource: FunctionReference< |
| 606 | 694 | "mutation", |
| 607 | 695 | "internal", |
| @@ -721,7 +809,6 @@ export declare const internal: { · 113 unmodified lines | ||
| 721 | 809 | { |
| 722 | 810 | error?: string; |
| 723 | 811 | exitCode?: number; |
| 724 | − result?: any; | |
| 725 | 812 | status: "completed" | "failed" | "timed_out" | "denied"; |
| 726 | 813 | taskId: string; |
| 727 | 814 | }, |
| @@ -733,6 +820,94 @@ export declare const internal: { · 5 unmodified lines | ||
| 733 | 820 | { taskId: string }, |
| 734 | 821 | 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 | 911 | purgeExpiredAnonymousOauthAuthorizationCodes: FunctionReference< |
| 737 | 912 | "mutation", |
| 738 | 913 | "internal", |
| @@ -800,6 +975,142 @@ export declare const internal: { · 61 unmodified lines | ||
| 800 | 975 | }, |
| 801 | 976 | 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 | 1114 | upsertAccessPolicy: FunctionReference< |
| 804 | 1115 | "mutation", |
| 805 | 1116 | "internal", |
| @@ -854,320 +1165,6 @@ export declare const internal: { · 48 unmodified lines | ||
| 854 | 1165 | 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 | 1168 | executor: { |
| 1172 | 1169 | completeRuntimeRun: FunctionReference< |
| 1173 | 1170 | "mutation", |
| @@ -1176,7 +1173,6 @@ export declare const internal: { · 2 unmodified lines | ||
| 1176 | 1173 | durationMs?: number; |
| 1177 | 1174 | error?: string; |
| 1178 | 1175 | exitCode?: number; |
| 1179 | − result?: any; | |
| 1180 | 1176 | runId: string; |
| 1181 | 1177 | status: "completed" | "failed" | "timed_out" | "denied"; |
| 1182 | 1178 | }, |
| @@ -3,7 +3,7 @@ import type { DbCtx } from "./types"; · 2 unmodified lines | ||
| 3 | 3 | |
| 4 | 4 | type UpsertWorkosAccountArgs = { |
| 5 | 5 | workosUserId: string; |
| 6 | − email: string; | |
| 6 | + email?: string; | |
| 7 | 7 | fullName: string; |
| 8 | 8 | firstName?: string; |
| 9 | 9 | lastName?: string; |
| @@ -15,13 +15,13 @@ type UpsertWorkosAccountArgs = { · 5 unmodified lines | ||
| 15 | 15 | export async function upsertWorkosAccount(ctx: DbCtx, args: UpsertWorkosAccountArgs) { |
| 16 | 16 | let account = await getAccountByWorkosId(ctx, args.workosUserId); |
| 17 | 17 | const patchData = { |
| 18 | − email: args.email, | |
| 19 | 18 | name: args.fullName, |
| 20 | − firstName: args.firstName, | |
| 21 | − lastName: args.lastName, | |
| 22 | − avatarUrl: args.avatarUrl, | |
| 23 | 19 | status: "active" as const, |
| 24 | 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 | 25 | ...(args.includeLastLoginAt ? { lastLoginAt: args.now } : null), |
| 26 | 26 | }; |
| 27 | 27 | |
| @@ -16,7 +16,7 @@ async function seedHintedOrganizationMembership( · 15 unmodified lines | ||
| 16 | 16 | args: { |
| 17 | 17 | accountId: AccountId; |
| 18 | 18 | hintedWorkosOrgId?: string; |
| 19 | − email: string; | |
| 19 | + email?: string; | |
| 20 | 20 | now: number; |
| 21 | 21 | }, |
| 22 | 22 | ) { |
| @@ -0,0 +1,47 @@ | ||
| 1 | +import { expect, test } from "bun:test"; | |
| 2 | +import { resolveIdentityProfile } from "./identity"; | |
| 3 | + | |
| 4 | +test("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 | + | |
| 16 | +test("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 | + | |
| 37 | +test("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 | +}); | |
| @@ -8,6 +8,10 @@ type WorkosProfile = { · 7 unmodified lines | ||
| 8 | 8 | profilePictureUrl?: string | null; |
| 9 | 9 | }; |
| 10 | 10 | |
| 11 | +function deriveFallbackUserLabel(workosUserId: string): string { | |
| 12 | + return `User ${workosUserId.slice(-6)}`; | |
| 13 | +} | |
| 14 | + | |
| 11 | 15 | export function getIdentityString(identity: Record<string, unknown>, keys: string[]): string | undefined { |
| 12 | 16 | for (const key of keys) { |
| 13 | 17 | const value = identity[key]; |
| @@ -36,28 +40,26 @@ export function resolveIdentityProfile(args: { · 22 unmodified lines | ||
| 36 | 40 | |
| 37 | 41 | const email = |
| 38 | 42 | authKitProfile?.email |
| 39 | − ?? getIdentityString(identity, ["email", "https://workos.com/email", "upn"]) | |
| 40 | − ?? `${identity.subject}@workos.executor.local`; | |
| 43 | + ?? getIdentityString(identity, ["email", "upn"]); | |
| 41 | 44 | |
| 42 | 45 | 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 | 48 | 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 | 53 | const fullName = |
| 49 | − (getIdentityString(identity, ["name", "https://workos.com/name"]) ?? [firstName, lastName].filter(Boolean).join(" ")) | |
| 50 | − || email; | |
| 54 | + getIdentityString(identity, ["name", "full_name"]) | |
| 55 | + ?? (combinedName.length > 0 ? combinedName : undefined) | |
| 56 | + ?? deriveFallbackUserLabel(identity.subject); | |
| 51 | 57 | |
| 52 | 58 | const avatarUrl = |
| 53 | 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, [ | |
| 57 | − "org_id", | |
| 58 | − "organization_id", | |
| 59 | − "https://workos.com/organization_id", | |
| 60 | − ]); | |
| 62 | + const hintedWorkosOrgId = getIdentityString(identity, ["org_id", "organization_id"]); | |
| 61 | 63 | |
| 62 | 64 | return { |
| 63 | 65 | email, |
| @@ -38,7 +38,7 @@ export function isGeneratedPersonalWorkspaceName(name: string, workosUserId: str · 37 unmodified lines | ||
| 38 | 38 | return new RegExp(`^${escapeRegex(workosUserId)}'s workspace$`, "i").test(name); |
| 39 | 39 | } |
| 40 | 40 | |
| 41 | −function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email: string; workosUserId: string }): string { | |
| 41 | +function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email?: string; workosUserId: string }): string { | |
| 42 | 42 | const firstName = args.firstName?.trim(); |
| 43 | 43 | if (firstName && !/^my$/i.test(firstName)) { |
| 44 | 44 | return firstName; |
| @@ -49,7 +49,7 @@ function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email: · 4 unmodified lines | ||
| 49 | 49 | return fullName; |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | − const emailLocalPart = args.email.split("@")[0]?.trim(); | |
| 52 | + const emailLocalPart = args.email?.split("@")[0]?.trim(); | |
| 53 | 53 | if (emailLocalPart) { |
| 54 | 54 | const normalized = emailLocalPart |
| 55 | 55 | .replace(/[._-]+/g, " ") |
| @@ -64,7 +64,7 @@ function deriveOwnerLabel(args: { firstName?: string; fullName?: string; email: · 8 unmodified lines | ||
| 64 | 64 | return `User ${args.workosUserId.slice(-6)}`; |
| 65 | 65 | } |
| 66 | 66 | |
| 67 | −export function derivePersonalNames(args: { firstName?: string; fullName?: string; email: string; workosUserId: string }) { | |
| 67 | +export function derivePersonalNames(args: { firstName?: string; fullName?: string; email?: string; workosUserId: string }) { | |
| 68 | 68 | const ownerLabel = deriveOwnerLabel(args); |
| 69 | 69 | return { |
| 70 | 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 | |
| 75 | −export function buildPersonalWorkspaceSlugSeed(email: string, workosUserId: string): string { | |
| 76 | − const baseSlug = slugify(email.split("@")[0] ?? workosUserId, "workspace"); | |
| 75 | +export function buildPersonalWorkspaceSlugSeed(email: string | undefined, workosUserId: string): string { | |
| 76 | + const baseSlug = slugify(email?.split("@")[0] ?? workosUserId, "workspace"); | |
| 77 | 77 | return `${baseSlug}-${workosUserId.slice(-6)}`; |
| 78 | 78 | } |
| 79 | 79 | |
| @@ -10,7 +10,7 @@ import { · 9 unmodified lines | ||
| 10 | 10 | import type { AccountId, DbCtx } from "./types"; |
| 11 | 11 | |
| 12 | 12 | type PersonalWorkspaceOptions = { |
| 13 | − email: string; | |
| 13 | + email?: string; | |
| 14 | 14 | firstName?: string; |
| 15 | 15 | fullName?: string; |
| 16 | 16 | workosUserId: string; |
| @@ -160,7 +160,7 @@ export const createCustomerPortal = action({ · 159 unmodified lines | ||
| 160 | 160 | handler: async (ctx, args): Promise<{ url: string }> => { |
| 161 | 161 | const access: { |
| 162 | 162 | role: string; |
| 163 | − email: string; | |
| 163 | + email?: string; | |
| 164 | 164 | organizationName: string; |
| 165 | 165 | billableMembers: number; |
| 166 | 166 | customerId: string | null; |
| @@ -59,7 +59,6 @@ test("task lifecycle supports queue, run, and complete", async () => { · 58 unmodified lines | ||
| 59 | 59 | const finished = await t.mutation(internal.database.markTaskFinished as any, { |
| 60 | 60 | taskId: "task_1", |
| 61 | 61 | status: "completed", |
| 62 | − result: { ok: true }, | |
| 63 | 62 | exitCode: 0, |
| 64 | 63 | }); |
| 65 | 64 | expect(finished?.status).toBe("completed"); |
| @@ -58,7 +58,6 @@ export function mapTask(doc: Doc<"tasks">) { · 57 unmodified lines | ||
| 58 | 58 | startedAt: doc.startedAt, |
| 59 | 59 | completedAt: doc.completedAt, |
| 60 | 60 | error: doc.error, |
| 61 | − result: doc.result, | |
| 62 | 61 | exitCode: doc.exitCode, |
| 63 | 62 | }; |
| 64 | 63 | } |
| @@ -1,23 +1,13 @@ | ||
| 1 | 1 | import { v } from "convex/values"; |
| 2 | 2 | import { internalMutation, internalQuery } from "../_generated/server"; |
| 3 | +import { listRuntimeTargets as listAvailableRuntimeTargets } from "../../core/src/runtimes/runtime-catalog"; | |
| 3 | 4 | import { mapPolicy } from "./mappers"; |
| 4 | 5 | import { policyDecisionValidator } from "./validators"; |
| 5 | 6 | |
| 6 | 7 | export const listRuntimeTargets = internalQuery({ |
| 7 | 8 | args: {}, |
| 8 | 9 | handler: async () => { |
| 9 | − return [ | |
| 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 | − ]; | |
| 10 | + return listAvailableRuntimeTargets(); | |
| 21 | 11 | }, |
| 22 | 12 | }); |
| 23 | 13 | |
| @@ -115,7 +115,6 @@ export const markTaskFinished = internalMutation({ · 114 unmodified lines | ||
| 115 | 115 | args: { |
| 116 | 116 | taskId: v.string(), |
| 117 | 117 | status: completedTaskStatusValidator, |
| 118 | − result: v.optional(v.any()), | |
| 119 | 118 | exitCode: v.optional(v.number()), |
| 120 | 119 | error: v.optional(v.string()), |
| 121 | 120 | }, |
| @@ -132,7 +131,6 @@ export const markTaskFinished = internalMutation({ · 10 unmodified lines | ||
| 132 | 131 | const now = Date.now(); |
| 133 | 132 | await ctx.db.patch(doc._id, { |
| 134 | 133 | status: args.status, |
| 135 | − result: args.result, | |
| 136 | 134 | exitCode: args.exitCode, |
| 137 | 135 | error: args.error, |
| 138 | 136 | completedAt: now, |
| @@ -5,7 +5,7 @@ import type { MutationCtx } from "./_generated/server"; · 4 unmodified lines | ||
| 5 | 5 | import { internalMutation } from "./_generated/server"; |
| 6 | 6 | import { workspaceMutation } from "../core/src/function-builders"; |
| 7 | 7 | import { actorIdForAccount } from "../core/src/identity"; |
| 8 | −import { isKnownRuntimeId } from "../core/src/runtimes/runtime-catalog"; | |
| 8 | +import { defaultRuntimeId, isKnownRuntimeId, isRuntimeEnabled } from "../core/src/runtimes/runtime-catalog"; | |
| 9 | 9 | import type { ApprovalRecord, TaskRecord } from "../core/src/types"; |
| 10 | 10 | import { isTerminalTaskStatus, taskTerminalEventType } from "./task/status"; |
| 11 | 11 | import { DEFAULT_TASK_TIMEOUT_MS } from "./task/constants"; |
| @@ -29,10 +29,13 @@ async function createTaskRecord( · 17 unmodified lines | ||
| 29 | 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 | 33 | if (!isKnownRuntimeId(runtimeId)) { |
| 34 | 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 | 40 | const taskId = `task_${crypto.randomUUID()}`; |
| 38 | 41 | const task = (await ctx.runMutation(internal.database.createTask, { |
| @@ -212,7 +215,6 @@ export const completeRuntimeRun = internalMutation({ · 173 unmodified lines | ||
| 212 | 215 | args: { |
| 213 | 216 | runId: v.string(), |
| 214 | 217 | status: v.union(v.literal("completed"), v.literal("failed"), v.literal("timed_out"), v.literal("denied")), |
| 215 | − result: v.optional(v.any()), | |
| 216 | 218 | exitCode: v.optional(v.number()), |
| 217 | 219 | error: v.optional(v.string()), |
| 218 | 220 | durationMs: v.optional(v.number()), |
| @@ -230,7 +232,6 @@ export const completeRuntimeRun = internalMutation({ · 11 unmodified lines | ||
| 230 | 232 | const finished = await markTaskFinished(ctx, { |
| 231 | 233 | taskId: args.runId, |
| 232 | 234 | status: args.status, |
| 233 | − result: args.result, | |
| 234 | 235 | exitCode: args.exitCode, |
| 235 | 236 | error: args.error, |
| 236 | 237 | }); |
| @@ -4,6 +4,7 @@ import type { Id } from "../_generated/dataModel"; · 3 unmodified lines | ||
| 4 | 4 | import type { |
| 5 | 5 | AnonymousContext, |
| 6 | 6 | PendingApprovalRecord, |
| 7 | + TaskExecutionOutcome, | |
| 7 | 8 | TaskRecord, |
| 8 | 9 | ToolDescriptor, |
| 9 | 10 | } from "../../core/src/types"; |
| @@ -25,7 +26,7 @@ export function createMcpExecutorService(ctx: ActionCtx) { · 15 unmodified lines | ||
| 25 | 26 | }; |
| 26 | 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 | 30 | return await ctx.runAction(internal.executorNode.runTask, { taskId }); |
| 30 | 31 | }, |
| 31 | 32 | getTask: async (taskId: string, workspaceId?: Id<"workspaces">): Promise<TaskRecord | null> => { |
| @@ -9,9 +9,10 @@ import { · 8 unmodified lines | ||
| 9 | 9 | CLOUDFLARE_WORKER_LOADER_RUNTIME_ID, |
| 10 | 10 | isCloudflareWorkerLoaderConfigured, |
| 11 | 11 | isKnownRuntimeId, |
| 12 | + isRuntimeEnabled, | |
| 12 | 13 | } from "../../core/src/runtimes/runtime-catalog"; |
| 13 | 14 | import { runCodeWithAdapter } from "../../core/src/runtimes/runtime-core"; |
| 14 | −import type { TaskRecord } from "../../core/src/types"; | |
| 15 | +import type { TaskExecutionOutcome, TaskRecord } from "../../core/src/types"; | |
| 15 | 16 | import { describeError } from "../../core/src/utils"; |
| 16 | 17 | import { publishTaskEvent } from "./events"; |
| 17 | 18 | import { taskTerminalEventType } from "../task/status"; |
| @@ -21,7 +22,7 @@ import { invokeTool } from "./tool_invocation"; · 3 unmodified lines | ||
| 21 | 22 | async function markTaskFailedAndPublish( |
| 22 | 23 | ctx: ActionCtx, |
| 23 | 24 | args: { taskId: string; error: string }, |
| 24 | −): Promise<void> { | |
| 25 | +): Promise<TaskExecutionOutcome | null> { | |
| 25 | 26 | const failed = await markTaskFinished(ctx, { |
| 26 | 27 | taskId: args.taskId, |
| 27 | 28 | status: "failed", |
| @@ -29,7 +30,7 @@ async function markTaskFailedAndPublish( · 1 unmodified line | ||
| 29 | 30 | }); |
| 30 | 31 | |
| 31 | 32 | if (!failed) { |
| 32 | − return; | |
| 33 | + return null; | |
| 33 | 34 | } |
| 34 | 35 | |
| 35 | 36 | await publishTaskEvent(ctx, args.taskId, "task", "task.failed", { |
| @@ -38,6 +39,8 @@ async function markTaskFailedAndPublish( · 2 unmodified lines | ||
| 38 | 39 | error: failed.error, |
| 39 | 40 | ...(failed.completedAt ? { completedAt: failed.completedAt } : {}), |
| 40 | 41 | }); |
| 42 | + | |
| 43 | + return { task: failed }; | |
| 41 | 44 | } |
| 42 | 45 | |
| 43 | 46 | async function publishTerminalTaskResult( |
| @@ -62,26 +65,31 @@ async function publishTerminalTaskResult( · 18 unmodified lines | ||
| 62 | 65 | export async function runQueuedTask( |
| 63 | 66 | ctx: ActionCtx, |
| 64 | 67 | args: { taskId: string }, |
| 65 | −): Promise<null> { | |
| 68 | +): Promise<TaskExecutionOutcome | null> { | |
| 66 | 69 | const task = (await ctx.runQuery(internal.database.getTask, { taskId: args.taskId })) as TaskRecord | null; |
| 67 | 70 | if (!task || task.status !== "queued") { |
| 68 | 71 | return null; |
| 69 | 72 | } |
| 70 | 73 | |
| 71 | 74 | if (!isKnownRuntimeId(task.runtimeId)) { |
| 72 | − await markTaskFailedAndPublish(ctx, { | |
| 75 | + return await markTaskFailedAndPublish(ctx, { | |
| 73 | 76 | taskId: args.taskId, |
| 74 | 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 | 88 | if (task.runtimeId === CLOUDFLARE_WORKER_LOADER_RUNTIME_ID && !isCloudflareWorkerLoaderConfigured()) { |
| 80 | − await markTaskFailedAndPublish(ctx, { | |
| 89 | + return await markTaskFailedAndPublish(ctx, { | |
| 81 | 90 | taskId: args.taskId, |
| 82 | 91 | error: `Runtime is not configured: ${task.runtimeId}`, |
| 83 | 92 | }); |
| 84 | − return null; | |
| 85 | 93 | } |
| 86 | 94 | |
| 87 | 95 | try { |
| @@ -106,17 +114,15 @@ export async function runQueuedTask( · 18 unmodified lines | ||
| 106 | 114 | }); |
| 107 | 115 | |
| 108 | 116 | if (!dispatchResult.ok) { |
| 109 | − await markTaskFailedAndPublish(ctx, { | |
| 117 | + return await markTaskFailedAndPublish(ctx, { | |
| 110 | 118 | taskId: args.taskId, |
| 111 | 119 | error: dispatchResult.error, |
| 112 | 120 | }); |
| 113 | − return null; | |
| 114 | 121 | } |
| 115 | 122 | |
| 116 | 123 | const finished = await markTaskFinished(ctx, { |
| 117 | 124 | taskId: args.taskId, |
| 118 | 125 | status: dispatchResult.status, |
| 119 | − result: dispatchResult.result, | |
| 120 | 126 | exitCode: dispatchResult.exitCode, |
| 121 | 127 | error: dispatchResult.error, |
| 122 | 128 | }); |
| @@ -131,7 +137,11 @@ export async function runQueuedTask( · 8 unmodified lines | ||
| 131 | 137 | finished, |
| 132 | 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 | 147 | const runtimeResult = await (async () => { |
| @@ -153,7 +163,6 @@ export async function runQueuedTask( · 15 unmodified lines | ||
| 153 | 163 | const finished = await markTaskFinished(ctx, { |
| 154 | 164 | taskId: args.taskId, |
| 155 | 165 | status: runtimeResult.status, |
| 156 | − result: runtimeResult.result, | |
| 157 | 166 | exitCode: runtimeResult.exitCode, |
| 158 | 167 | error: runtimeResult.error, |
| 159 | 168 | }); |
| @@ -168,6 +177,11 @@ export async function runQueuedTask( · 8 unmodified lines | ||
| 168 | 177 | finished, |
| 169 | 178 | durationMs: runtimeResult.durationMs, |
| 170 | 179 | }); |
| 180 | + return { | |
| 181 | + task: finished, | |
| 182 | + result: runtimeResult.result, | |
| 183 | + durationMs: runtimeResult.durationMs, | |
| 184 | + }; | |
| 171 | 185 | } catch (error) { |
| 172 | 186 | const message = describeError(error); |
| 173 | 187 | const denied = message.startsWith(APPROVAL_DENIED_PREFIX); |
| @@ -183,6 +197,7 @@ export async function runQueuedTask( · 9 unmodified lines | ||
| 183 | 197 | status: denied ? "denied" : "failed", |
| 184 | 198 | finished, |
| 185 | 199 | }); |
| 200 | + return { task: finished }; | |
| 186 | 201 | } |
| 187 | 202 | } |
| 188 | 203 | |
| @@ -11,7 +11,6 @@ import { · 10 unmodified lines | ||
| 11 | 11 | type WorkspaceToolSnapshot, |
| 12 | 12 | } from "../../core/src/tool-sources"; |
| 13 | 13 | import type { ExternalToolSourceConfig } from "../../core/src/tool-source-types"; |
| 14 | −import { DEFAULT_TOOLS } from "../../core/src/tools"; | |
| 15 | 14 | import type { |
| 16 | 15 | AccessPolicyRecord, |
| 17 | 16 | OpenApiSourceQuality, |
| @@ -22,7 +21,7 @@ import type { · 4 unmodified lines | ||
| 22 | 21 | import { computeOpenApiSourceQuality, listVisibleToolDescriptors } from "./tool_descriptors"; |
| 23 | 22 | import { loadSourceArtifact, normalizeExternalToolSource, sourceSignature } from "./tool_source_loading"; |
| 24 | 23 | |
| 25 | −const baseTools = new Map<string, ToolDefinition>(DEFAULT_TOOLS.map((tool) => [tool.path, tool])); | |
| 24 | +const baseTools = new Map<string, ToolDefinition>(); | |
| 26 | 25 | |
| 27 | 26 | export interface DtsStorageEntry { |
| 28 | 27 | sourceKey: string; |
| @@ -37,7 +37,6 @@ export const completeRun = mutation({ · 36 unmodified lines | ||
| 37 | 37 | internalSecret: v.string(), |
| 38 | 38 | runId: v.string(), |
| 39 | 39 | status: v.union(v.literal("completed"), v.literal("failed"), v.literal("timed_out"), v.literal("denied")), |
| 40 | − result: v.optional(v.any()), | |
| 41 | 40 | exitCode: v.optional(v.number()), |
| 42 | 41 | error: v.optional(v.string()), |
| 43 | 42 | durationMs: v.optional(v.number()), |
| @@ -48,7 +47,6 @@ export const completeRun = mutation({ · 4 unmodified lines | ||
| 48 | 47 | return await ctx.runMutation(internal.executor.completeRuntimeRun, { |
| 49 | 48 | runId: args.runId, |
| 50 | 49 | status: args.status, |
| 51 | − result: args.result, | |
| 52 | 50 | exitCode: args.exitCode, |
| 53 | 51 | error: args.error, |
| 54 | 52 | durationMs: args.durationMs, |
| @@ -53,7 +53,7 @@ export default defineSchema({ · 52 unmodified lines | ||
| 53 | 53 | accounts: defineTable({ |
| 54 | 54 | provider: accountProvider, |
| 55 | 55 | providerAccountId: v.string(), // WorkOS user ID or anon_* UUID |
| 56 | − email: v.string(), | |
| 56 | + email: v.optional(v.string()), | |
| 57 | 57 | name: v.string(), |
| 58 | 58 | firstName: v.optional(v.string()), |
| 59 | 59 | lastName: v.optional(v.string()), |
| @@ -188,7 +188,6 @@ export default defineSchema({ · 128 unmodified lines | ||
| 188 | 188 | error: v.optional(v.string()), |
| 189 | 189 | stdout: v.optional(v.string()), |
| 190 | 190 | stderr: v.optional(v.string()), |
| 191 | − result: v.optional(v.any()), | |
| 192 | 191 | exitCode: v.optional(v.number()), |
| 193 | 192 | createdAt: v.number(), |
| 194 | 193 | updatedAt: v.number(), |
| @@ -8,7 +8,6 @@ type TaskFinishContext = Pick<ActionCtx, "runMutation"> | Pick<MutationCtx, "run · 7 unmodified lines | ||
| 8 | 8 | export interface MarkTaskFinishedInput { |
| 9 | 9 | taskId: string; |
| 10 | 10 | status: TerminalTaskStatus; |
| 11 | − result?: unknown; | |
| 12 | 11 | exitCode?: number; |
| 13 | 12 | error?: string; |
| 14 | 13 | } |
| @@ -3,6 +3,7 @@ import type { · 2 unmodified lines | ||
| 3 | 3 | AnonymousContext, |
| 4 | 4 | CreateTaskInput, |
| 5 | 5 | PendingApprovalRecord, |
| 6 | + TaskExecutionOutcome, | |
| 6 | 7 | TaskRecord, |
| 7 | 8 | ToolDescriptor, |
| 8 | 9 | } from "./types"; |
| @@ -10,7 +11,7 @@ import type { Id } from "../../convex/_generated/dataModel"; · 1 unmodified line | ||
| 10 | 11 | |
| 11 | 12 | export interface McpExecutorService { |
| 12 | 13 | createTask(input: CreateTaskInput): Promise<{ task: TaskRecord }>; |
| 13 | − runTaskNow?(taskId: string): Promise<null>; | |
| 14 | + runTaskNow?(taskId: string): Promise<TaskExecutionOutcome | null>; | |
| 14 | 15 | getTask(taskId: string, workspaceId?: Id<"workspaces">): Promise<TaskRecord | null>; |
| 15 | 16 | subscribe(taskId: string, workspaceId: Id<"workspaces">, listener: (event: LiveTaskEvent) => void): () => void; |
| 16 | 17 | bootstrapAnonymousContext(sessionId?: string): Promise<AnonymousContext>; |
| @@ -15,7 +15,7 @@ function listTopLevelToolKeys(tools: ToolDescriptor[]): string[] { · 14 unmodified lines | ||
| 15 | 15 | return [...keys].sort(); |
| 16 | 16 | } |
| 17 | 17 | |
| 18 | −export function summarizeTask(task: TaskRecord): string { | |
| 18 | +export function summarizeTask(task: TaskRecord, result: unknown = task.result): string { | |
| 19 | 19 | const maxResultPreviewChars = 30_000; |
| 20 | 20 | const lines = [ |
| 21 | 21 | `taskId: ${task.id}`, |
| @@ -32,8 +32,8 @@ export function summarizeTask(task: TaskRecord): string { · 10 unmodified lines | ||
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | let text = lines.join("\n"); |
| 35 | − if (task.result !== undefined) { | |
| 36 | − const serialized = Result.try(() => JSON.stringify(task.result, null, 2)).unwrapOr(String(task.result)); | |
| 35 | + if (result !== undefined) { | |
| 36 | + const serialized = Result.try(() => JSON.stringify(result, null, 2)).unwrapOr(String(result)); | |
| 37 | 37 | if (serialized.length > maxResultPreviewChars) { |
| 38 | 38 | text += asCodeBlock( |
| 39 | 39 | "json", |
| @@ -87,8 +87,8 @@ function createRunCodeTool( · 86 unmodified lines | ||
| 87 | 87 | } |
| 88 | 88 | |
| 89 | 89 | if (service.runTaskNow) { |
| 90 | − await service.runTaskNow(created.task.id); | |
| 91 | − const task = await service.getTask(created.task.id, context.workspaceId); | |
| 90 | + const runOutcome = await service.runTaskNow(created.task.id); | |
| 91 | + const task = runOutcome?.task ?? await service.getTask(created.task.id, context.workspaceId); | |
| 92 | 92 | if (!task) { |
| 93 | 93 | return { |
| 94 | 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 | 100 | const isError = task.status !== "completed"; |
| 100 | 101 | return { |
| 101 | − content: [textContent(summarizeTask(task))], | |
| 102 | + content: [textContent(summarizeTask(task, result))], | |
| 102 | 103 | structuredContent: { |
| 103 | 104 | taskId: task.id, |
| 104 | 105 | status: task.status, |
| 105 | 106 | runtimeId: task.runtimeId, |
| 106 | 107 | exitCode: task.exitCode, |
| 107 | 108 | error: task.error, |
| 108 | − result: task.result, | |
| 109 | + result, | |
| 109 | 110 | workspaceId: context.workspaceId, |
| 110 | 111 | actorId: context.actorId, |
| 111 | 112 | sessionId: context.sessionId, |
| @@ -153,6 +153,35 @@ describe("runtime catalog", () => { · 152 unmodified lines | ||
| 153 | 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 | 185 | test("getCloudflareWorkerLoaderConfig reads env vars", async () => { |
| 157 | 186 | const { getCloudflareWorkerLoaderConfig } = await import("./runtime-catalog"); |
| 158 | 187 | const config = getCloudflareWorkerLoaderConfig(); |
| @@ -2,6 +2,9 @@ · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | export const LOCAL_BUN_RUNTIME_ID = "local-bun"; |
| 4 | 4 | export const CLOUDFLARE_WORKER_LOADER_RUNTIME_ID = "cloudflare-worker-loader"; |
| 5 | +export const CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY = "EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY"; | |
| 6 | + | |
| 7 | +const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]); | |
| 5 | 8 | |
| 6 | 9 | const KNOWN_RUNTIME_IDS = new Set([ |
| 7 | 10 | LOCAL_BUN_RUNTIME_ID, |
| @@ -12,6 +15,56 @@ export function isKnownRuntimeId(runtimeId: string): boolean { · 4 unmodified lines | ||
| 12 | 15 | return KNOWN_RUNTIME_IDS.has(runtimeId); |
| 13 | 16 | } |
| 14 | 17 | |
| 18 | +function isTruthyEnvValue(value: string | undefined): boolean { | |
| 19 | + if (!value) { | |
| 20 | + return false; | |
| 21 | + } | |
| 22 | + return TRUTHY_ENV_VALUES.has(value.trim().toLowerCase()); | |
| 23 | +} | |
| 24 | + | |
| 25 | +export function isCloudflareDynamicWorkerOnlyMode(): boolean { | |
| 26 | + return isTruthyEnvValue(process.env[CLOUDFLARE_DYNAMIC_WORKER_ONLY_ENV_KEY]); | |
| 27 | +} | |
| 28 | + | |
| 29 | +export 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 | + | |
| 39 | +export function defaultRuntimeId(): string { | |
| 40 | + return isCloudflareDynamicWorkerOnlyMode() | |
| 41 | + ? CLOUDFLARE_WORKER_LOADER_RUNTIME_ID | |
| 42 | + : LOCAL_BUN_RUNTIME_ID; | |
| 43 | +} | |
| 44 | + | |
| 45 | +export interface RuntimeTargetDescriptor { | |
| 46 | + id: string; | |
| 47 | + label: string; | |
| 48 | + description: string; | |
| 49 | +} | |
| 50 | + | |
| 51 | +const 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 | + | |
| 64 | +export function listRuntimeTargets(): RuntimeTargetDescriptor[] { | |
| 65 | + return RUNTIME_TARGETS.filter((target) => isRuntimeEnabled(target.id)); | |
| 66 | +} | |
| 67 | + | |
| 15 | 68 | // ── Cloudflare Worker Loader config ────────────────────────────────────────── |
| 16 | 69 | |
| 17 | 70 | export interface CloudflareWorkerLoaderConfig { |
| @@ -1,82 +0,0 @@ | ||
| 1 | −import type { ToolDefinition } from "./types"; | |
| 2 | −import { asRecord } from "./utils"; | |
| 3 | − | |
| 4 | −export 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 | −]; | |
| @@ -39,6 +39,12 @@ export interface TaskRecord { · 38 unmodified lines | ||
| 39 | 39 | exitCode?: number; |
| 40 | 40 | } |
| 41 | 41 | |
| 42 | +export interface TaskExecutionOutcome { | |
| 43 | + task: TaskRecord; | |
| 44 | + result?: unknown; | |
| 45 | + durationMs?: number; | |
| 46 | +} | |
| 47 | + | |
| 42 | 48 | export interface ApprovalRecord { |
| 43 | 49 | id: string; |
| 44 | 50 | taskId: string; |
| @@ -24,6 +24,7 @@ describe("buildEnvChecks", () => { · 23 unmodified lines | ||
| 24 | 24 | expect(byName.get("env:CLOUDFLARE_SANDBOX_RUN_URL")?.ok).toBe(false); |
| 25 | 25 | expect(byName.get("env:CLOUDFLARE_SANDBOX_AUTH_TOKEN")?.ok).toBe(false); |
| 26 | 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 | 28 | expect(byName.get("env:CONVEX_URL")?.ok).toBe(false); |
| 28 | 29 | expect(byName.get("env:WORKOS_CLIENT_ID")?.ok).toBe(false); |
| 29 | 30 | expect(byName.get("env:WORKOS_API_KEY")?.ok).toBe(false); |
| @@ -40,6 +41,7 @@ describe("buildEnvChecks", () => { · 10 unmodified lines | ||
| 40 | 41 | ["CLOUDFLARE_SANDBOX_RUN_URL", "https://executor-sandbox-host.example.workers.dev/v1/runs"], |
| 41 | 42 | ["CLOUDFLARE_SANDBOX_AUTH_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"], |
| 42 | 43 | ["EXECUTOR_INTERNAL_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"], |
| 44 | + ["EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY", "1"], | |
| 43 | 45 | ["CONVEX_URL", "https://perceptive-pigeon-577.convex.site"], |
| 44 | 46 | ["WORKOS_CLIENT_ID", "client_123"], |
| 45 | 47 | ["WORKOS_API_KEY", "sk_test_123"], |
| @@ -61,6 +63,7 @@ describe("buildEnvChecks", () => { · 15 unmodified lines | ||
| 61 | 63 | ["CLOUDFLARE_SANDBOX_RUN_URL", "https://executor-sandbox-host.example.workers.dev/v1/runs"], |
| 62 | 64 | ["CLOUDFLARE_SANDBOX_AUTH_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"], |
| 63 | 65 | ["EXECUTOR_INTERNAL_TOKEN", "abcdefghijklmnopqrstuvwxyz0123456789"], |
| 66 | + ["EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY", "1"], | |
| 64 | 67 | ["CONVEX_URL", "https://perceptive-pigeon-577.convex.cloud"], |
| 65 | 68 | ["CONVEX_SITE_URL", "https://perceptive-pigeon-577.convex.site"], |
| 66 | 69 | ["WORKOS_CLIENT_ID", "client_123"], |
| @@ -77,4 +80,27 @@ describe("buildEnvChecks", () => { · 10 unmodified lines | ||
| 77 | 80 | const failures = checks.filter((check) => !check.ok); |
| 78 | 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 | }); |
| @@ -20,6 +20,7 @@ const REQUIRED_PROD_ENV_KEYS = [ · 19 unmodified lines | ||
| 20 | 20 | "CLOUDFLARE_SANDBOX_RUN_URL", |
| 21 | 21 | "CLOUDFLARE_SANDBOX_AUTH_TOKEN", |
| 22 | 22 | "EXECUTOR_INTERNAL_TOKEN", |
| 23 | + "EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY", | |
| 23 | 24 | "CONVEX_URL", |
| 24 | 25 | ] as const; |
| 25 | 26 | |
| @@ -113,6 +114,13 @@ function parseUrl(input: string): URL | null { · 87 unmodified lines | ||
| 113 | 114 | } |
| 114 | 115 | } |
| 115 | 116 | |
| 117 | +function isTruthy(value: string | undefined): boolean { | |
| 118 | + if (!value) { | |
| 119 | + return false; | |
| 120 | + } | |
| 121 | + return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase()); | |
| 122 | +} | |
| 123 | + | |
| 116 | 124 | function deploymentPrefix(hostname: string, suffix: ".convex.cloud" | ".convex.site"): string | null { |
| 117 | 125 | if (!hostname.endsWith(suffix)) { |
| 118 | 126 | return null; |
| @@ -254,6 +262,19 @@ export function buildEnvChecks(env: Map<string, string>): DoctorCheck[] { · 135 unmodified lines | ||
| 254 | 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 | 278 | for (const key of OPTIONAL_BILLING_ENV_KEYS) { |
| 258 | 279 | const value = env.get(key)?.trim(); |
| 259 | 280 | addCheck( |
| @@ -272,12 +293,19 @@ function guidanceForFailure(checkName: string): string[] { · 12 unmodified lines | ||
| 272 | 293 | checkName === "env:CLOUDFLARE_SANDBOX_RUN_URL" |
| 273 | 294 | || checkName === "env:CLOUDFLARE_SANDBOX_AUTH_TOKEN" |
| 274 | 295 | || checkName === "env:EXECUTOR_INTERNAL_TOKEN" |
| 296 | + || checkName === "env:EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY" | |
| 275 | 297 | ) { |
| 276 | 298 | return [ |
| 277 | 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 | 309 | if (checkName === "env:CONVEX_URL") { |
| 282 | 310 | return [ |
| 283 | 311 | "Set Convex API URL: `bunx convex env set CONVEX_URL https://<deployment>.convex.cloud --prod`.", |
| @@ -260,12 +260,14 @@ async function main(): Promise<void> { · 259 unmodified lines | ||
| 260 | 260 | await setConvexEnv("CLOUDFLARE_SANDBOX_RUN_URL", runUrl); |
| 261 | 261 | await setConvexEnv("CLOUDFLARE_SANDBOX_AUTH_TOKEN", authToken); |
| 262 | 262 | await setConvexEnv("EXECUTOR_INTERNAL_TOKEN", internalToken); |
| 263 | + await setConvexEnv("EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY", "1"); | |
| 263 | 264 | await setConvexEnv("CONVEX_URL", convexUrl); |
| 264 | 265 | |
| 265 | 266 | console.log("\n[setup] cloudflare production runtime configured"); |
| 266 | 267 | console.log(` CLOUDFLARE_SANDBOX_RUN_URL: ${runUrl}`); |
| 267 | 268 | console.log(` CLOUDFLARE_SANDBOX_AUTH_TOKEN: ${redact(authToken)}`); |
| 268 | 269 | console.log(` EXECUTOR_INTERNAL_TOKEN: ${redact(internalToken)}`); |
| 270 | + console.log(" EXECUTOR_CLOUDFLARE_DYNAMIC_WORKER_ONLY: 1"); | |
| 269 | 271 | console.log(` CONVEX_URL: ${convexUrl}`); |
| 270 | 272 | |
| 271 | 273 | if (options.runDoctor) { |