- 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 | CONVEX_URL=https://your-deployment.convex.cloud | 7 | CONVEX_URL=https://your-deployment.convex.cloud |
| 8 | CONVEX_SITE_URL=https://your-deployment.convex.site | 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 | # ── 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_... |
| @@ -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` |
| @@ -1,5 +1,5 @@ | |||
| 1 | import { StaticAppShell } from "@/components/static-app-shell"; | 1 | import { StaticAppShell } from "@/components/static-app-shell"; |
| 2 | 2 | ||
| 3 | export default function ApprovalsPage() { | 3 | export default function DashboardRoutesPage() { |
| 4 | return <StaticAppShell />; | 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 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useState } from "react"; | 3 | import { useEffect, useState } from "react"; |
| 4 | import { Play, Send } from "lucide-react"; | 4 | import { Play, Send } from "lucide-react"; |
| 5 | import { useMutation, useQuery } from "convex/react"; | 5 | import { useMutation, useQuery } from "convex/react"; |
| 6 | import { toast } from "sonner"; | 6 | import { toast } from "sonner"; |
| @@ -21,20 +21,12 @@ import { useSession } from "@/lib/session-context"; · 14 unmodified lines | |||
| 21 | import type { RuntimeTargetDescriptor } from "@/lib/types"; | 21 | import type { RuntimeTargetDescriptor } from "@/lib/types"; |
| 22 | import { useWorkspaceTools } from "@/hooks/use-workspace-tools"; | 22 | import { useWorkspaceTools } from "@/hooks/use-workspace-tools"; |
| 23 | 23 | ||
| 24 | const DEFAULT_CODE = `// Example: call some tools and return a compact result | 24 | const DEFAULT_CODE = `// Example: discover tools and return matching tool names |
| 25 | const time = await tools.utils.get_time(); | 25 | const found = await tools.discover({ |
| 26 | const sum = await tools.math.add({ a: 7, b: 35 }); | 26 | query: "Discover", |
| 27 | |||
| 28 | // This will require approval: | ||
| 29 | await tools.admin.send_announcement({ | ||
| 30 | channel: "general", | ||
| 31 | message: "Hello from executor!" | ||
| 32 | }); | 27 | }); |
| 33 | 28 | ||
| 34 | return { | 29 | return found.results.map((tool) => tool.path);`; |
| 35 | isoTime: time.iso, | ||
| 36 | total: sum.result, | ||
| 37 | };`; | ||
| 38 | const DEFAULT_TIMEOUT_MS = 300_000; | 30 | const DEFAULT_TIMEOUT_MS = 300_000; |
| 39 | 31 | ||
| 40 | export function TaskComposer() { | 32 | export 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> |
| @@ -11,6 +11,8 @@ const codePlugin = createCodePlugin({ · 10 unmodified lines | |||
| 11 | 11 | ||
| 12 | export function ToolDetail({ tool, depth }: { tool: ToolDescriptor; depth: number }) { | 12 | export 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 | } |
| @@ -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 | } |
| @@ -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 | 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 | } |
| @@ -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 | any | 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 | 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 | any | 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 | 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 | any | 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 | 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 | any | 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 | 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 | }, |
| @@ -3,7 +3,7 @@ import type { DbCtx } from "./types"; · 2 unmodified lines | |||
| 3 | 3 | ||
| 4 | type UpsertWorkosAccountArgs = { | 4 | type 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 | |||
| 15 | export async function upsertWorkosAccount(ctx: DbCtx, args: UpsertWorkosAccountArgs) { | 15 | export 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 | ||
| @@ -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 | ) { |
| @@ -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 | profilePictureUrl?: string | null; | 8 | profilePictureUrl?: string | null; |
| 9 | }; | 9 | }; |
| 10 | 10 | ||
| 11 | function deriveFallbackUserLabel(workosUserId: string): string { | ||
| 12 | return `User ${workosUserId.slice(-6)}`; | ||
| 13 | } | ||
| 14 | |||
| 11 | export function getIdentityString(identity: Record<string, unknown>, keys: string[]): string | undefined { | 15 | export 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?.email | 42 | 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?.firstName | 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 | const lastName = authKitProfile?.lastName | 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 | 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, |
| @@ -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 | ||
| 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 | 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 = emailLocalPart | 54 | 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 | ||
| 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 | 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 | ||
| 75 | export function buildPersonalWorkspaceSlugSeed(email: string, workosUserId: string): string { | 75 | export 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 | ||
| @@ -10,7 +10,7 @@ import { · 9 unmodified lines | |||
| 10 | import type { AccountId, DbCtx } from "./types"; | 10 | import type { AccountId, DbCtx } from "./types"; |
| 11 | 11 | ||
| 12 | type PersonalWorkspaceOptions = { | 12 | type 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; |
| @@ -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; |
| @@ -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"); |
| @@ -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 | } |
| @@ -1,23 +1,13 @@ | |||
| 1 | import { v } from "convex/values"; | 1 | import { v } from "convex/values"; |
| 2 | import { internalMutation, internalQuery } from "../_generated/server"; | 2 | import { internalMutation, internalQuery } from "../_generated/server"; |
| 3 | import { listRuntimeTargets as listAvailableRuntimeTargets } from "../../core/src/runtimes/runtime-catalog"; | ||
| 3 | import { mapPolicy } from "./mappers"; | 4 | import { mapPolicy } from "./mappers"; |
| 4 | import { policyDecisionValidator } from "./validators"; | 5 | import { policyDecisionValidator } from "./validators"; |
| 5 | 6 | ||
| 6 | export const listRuntimeTargets = internalQuery({ | 7 | export 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 | ||
| @@ -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, |
| @@ -5,7 +5,7 @@ import type { MutationCtx } from "./_generated/server"; · 4 unmodified lines | |||
| 5 | import { internalMutation } from "./_generated/server"; | 5 | import { internalMutation } from "./_generated/server"; |
| 6 | import { workspaceMutation } from "../core/src/function-builders"; | 6 | import { workspaceMutation } from "../core/src/function-builders"; |
| 7 | import { actorIdForAccount } from "../core/src/identity"; | 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 | import type { ApprovalRecord, TaskRecord } from "../core/src/types"; | 9 | import type { ApprovalRecord, TaskRecord } from "../core/src/types"; |
| 10 | import { isTerminalTaskStatus, taskTerminalEventType } from "./task/status"; | 10 | import { isTerminalTaskStatus, taskTerminalEventType } from "./task/status"; |
| 11 | import { DEFAULT_TASK_TIMEOUT_MS } from "./task/constants"; | 11 | import { 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 | }); |
| @@ -4,6 +4,7 @@ import type { Id } from "../_generated/dataModel"; · 3 unmodified lines | |||
| 4 | import type { | 4 | import 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> => { |
| @@ -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"; |
| 13 | import { runCodeWithAdapter } from "../../core/src/runtimes/runtime-core"; | 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 | import { describeError } from "../../core/src/utils"; | 16 | import { describeError } from "../../core/src/utils"; |
| 16 | import { publishTaskEvent } from "./events"; | 17 | import { publishTaskEvent } from "./events"; |
| 17 | import { taskTerminalEventType } from "../task/status"; | 18 | import { taskTerminalEventType } from "../task/status"; |
| @@ -21,7 +22,7 @@ import { invokeTool } from "./tool_invocation"; · 3 unmodified lines | |||
| 21 | async function markTaskFailedAndPublish( | 22 | async 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 | ||
| 43 | async function publishTerminalTaskResult( | 46 | async function publishTerminalTaskResult( |
| @@ -62,26 +65,31 @@ async function publishTerminalTaskResult( · 18 unmodified lines | |||
| 62 | export async function runQueuedTask( | 65 | export 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 | ||
| @@ -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"; |
| 13 | import type { ExternalToolSourceConfig } from "../../core/src/tool-source-types"; | 13 | import type { ExternalToolSourceConfig } from "../../core/src/tool-source-types"; |
| 14 | import { DEFAULT_TOOLS } from "../../core/src/tools"; | ||
| 15 | import type { | 14 | import type { |
| 16 | AccessPolicyRecord, | 15 | AccessPolicyRecord, |
| 17 | OpenApiSourceQuality, | 16 | OpenApiSourceQuality, |
| @@ -22,7 +21,7 @@ import type { · 4 unmodified lines | |||
| 22 | import { computeOpenApiSourceQuality, listVisibleToolDescriptors } from "./tool_descriptors"; | 21 | import { computeOpenApiSourceQuality, listVisibleToolDescriptors } from "./tool_descriptors"; |
| 23 | import { loadSourceArtifact, normalizeExternalToolSource, sourceSignature } from "./tool_source_loading"; | 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 | export interface DtsStorageEntry { | 26 | export interface DtsStorageEntry { |
| 28 | sourceKey: string; | 27 | sourceKey: string; |
| @@ -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, |
| @@ -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_* UUID | 55 | 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(), |
| @@ -8,7 +8,6 @@ type TaskFinishContext = Pick<ActionCtx, "runMutation"> | Pick<MutationCtx, "run · 7 unmodified lines | |||
| 8 | export interface MarkTaskFinishedInput { | 8 | export 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 | } |
| @@ -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 | ||
| 11 | export interface McpExecutorService { | 12 | export 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>; |
| @@ -15,7 +15,7 @@ function listTopLevelToolKeys(tools: ToolDescriptor[]): string[] { · 14 unmodified lines | |||
| 15 | return [...keys].sort(); | 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 | 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", |
| @@ -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, |
| @@ -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(); |
| @@ -2,6 +2,9 @@ · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | export const LOCAL_BUN_RUNTIME_ID = "local-bun"; | 3 | export const LOCAL_BUN_RUNTIME_ID = "local-bun"; |
| 4 | export const CLOUDFLARE_WORKER_LOADER_RUNTIME_ID = "cloudflare-worker-loader"; | 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 | const KNOWN_RUNTIME_IDS = new Set([ | 9 | const 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 | ||
| 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 | // ── Cloudflare Worker Loader config ────────────────────────────────────────── | 68 | // ── Cloudflare Worker Loader config ────────────────────────────────────────── |
| 16 | 69 | ||
| 17 | export interface CloudflareWorkerLoaderConfig { | 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 | exitCode?: number; | 39 | exitCode?: number; |
| 40 | } | 40 | } |
| 41 | 41 | ||
| 42 | export interface TaskExecutionOutcome { | ||
| 43 | task: TaskRecord; | ||
| 44 | result?: unknown; | ||
| 45 | durationMs?: number; | ||
| 46 | } | ||
| 47 | |||
| 42 | export interface ApprovalRecord { | 48 | export interface ApprovalRecord { |
| 43 | id: string; | 49 | id: string; |
| 44 | taskId: string; | 50 | taskId: string; |
| @@ -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 | }); |
| @@ -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 | ||
| 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 | function deploymentPrefix(hostname: string, suffix: ".convex.cloud" | ".convex.site"): string | null { | 124 | function 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`.", |
| @@ -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) { |