diff --git a/.github/workflows/publish-ee-images.yml b/.github/workflows/publish-ee-images.yml index ce051cc465..600091f9dd 100644 --- a/.github/workflows/publish-ee-images.yml +++ b/.github/workflows/publish-ee-images.yml @@ -30,6 +30,7 @@ on: - "packages/email/**" - "packages/types/**" - "packages/ui/**" + - "packages/ui-artifact-mcp/**" - "pnpm-lock.yaml" - "pnpm-workspace.yaml" - "package.json" diff --git a/apps/app/src/app/lib/den.ts b/apps/app/src/app/lib/den.ts index 635f5cee08..b189fcd7a8 100644 --- a/apps/app/src/app/lib/den.ts +++ b/apps/app/src/app/lib/den.ts @@ -2,6 +2,11 @@ import { normalizeDesktopConfig, type DesktopConfig as SharedDesktopConfig, } from "@openwork/types/den/desktop-policies"; +import { + uiArtifactPreferencesSchema, + type UiArtifactPreferences, + type UiArtifactPreferencesUpdate, +} from "@openwork/types/ui-artifact"; // Re-export the shared schema under the local alias so React consumers // (e.g. the cloud domain's desktop-config provider) can import it alongside @@ -2228,6 +2233,28 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul return normalizeDenDesktopConfig(payload); }, + async getUiArtifactPreferences(orgId: string): Promise { + const payload = await requestJson(baseUrls, "/v1/me/ui-artifacts", { + method: "GET", + token, + organizationId: orgId, + }); + return uiArtifactPreferencesSchema.parse(payload); + }, + + async updateUiArtifactPreferences( + orgId: string, + update: UiArtifactPreferencesUpdate, + ): Promise { + const payload = await requestJson(baseUrls, "/v1/me/ui-artifacts", { + method: "PUT", + token, + organizationId: orgId, + body: update, + }); + return uiArtifactPreferencesSchema.parse(payload); + }, + async getResourceSnapshot(orgId?: string | null): Promise { const payload = await requestJson(baseUrls, "/v1/resources", { method: "GET", diff --git a/apps/app/src/components/chat/message-list.tsx b/apps/app/src/components/chat/message-list.tsx index 7b759aedb3..e1c8fbe83c 100644 --- a/apps/app/src/components/chat/message-list.tsx +++ b/apps/app/src/components/chat/message-list.tsx @@ -39,6 +39,7 @@ import { OpenWorkSessionCreateTool } from "@/components/tools/openwork-session-c import { QuestionTool } from "@/components/tools/question" import { SkillTool } from "@/components/tools/skill" import { TodoWriteTool } from "@/components/tools/todowrite" +import { UiArtifactCard } from "@/components/tools/ui-artifact" import { WebfetchTool } from "@/components/tools/webfetch" import { WebsearchTool } from "@/components/tools/websearch" import { useMessageList, useSessionErrorMessage } from "@/components/chat/message-list-provider" @@ -107,6 +108,13 @@ import { import { faviconUrlForHref } from "@/lib/favicon" import { cn } from "@/lib/utils" import { groupMessages, isMessageGroup, getLastTextPart, getAggregateOnlyParts, getAssistantRenderGroups, getFileTitle, getMediaBadge, getMessageCompleted, getMessageCreated, formatMessageTimestamp, splitTurnAtAnswer, type UIMessageWithIndex, getMessagesText, getSafeFileDownloadUrl, getSafeFileRevealPath } from "./utils" +import { useUiArtifactPreferencesSnapshot } from "@/react-app/domains/settings/state/feature-flags-preferences" +import { + buildUiArtifactDecisionPrompt, + isUiArtifactRenderInvocation, + parseUiArtifactRenderResult, + reconcileUiArtifactMessages, +} from "@/lib/ui-artifacts" import type { AnyToolPart } from "@/lib/tool-aggregate" const SEARCH_HIGHLIGHT_MARK_CLASS = "rounded px-0.5 bg-amber-4/70 text-current" @@ -164,7 +172,25 @@ class ToolMessage extends React.Component } const ToolMessageInner = ({ part }: ToolMessageProps) => { - const { onMcpReconnect, onMcpReopenAuthorization, onMcpRetry } = useMessageList() + const { onMcpReconnect, onMcpReopenAuthorization, onMcpRetry, setPrompt } = useMessageList() + const { uiArtifactsEnabled, enabledUiArtifactIds } = useUiArtifactPreferencesSnapshot() + + if ( + uiArtifactsEnabled && + part.type === "dynamic-tool" && + part.state === "output-available" && + isUiArtifactRenderInvocation(part.toolName, part.input) + ) { + const result = parseUiArtifactRenderResult(part.output) + if (result && enabledUiArtifactIds.includes(result.artifact.artifactId)) { + return ( + setPrompt(buildUiArtifactDecisionPrompt(action))} + /> + ) + } + } if (isBashToolPart(part)) { return @@ -1144,17 +1170,22 @@ export function shouldShowMessageListLoading(status: ThreadStatus, messageCount: export function MessageList({ messages, status, retryStatus }: MessageListProps) { const isStreaming = status === "streaming" || status === "retrying" + const { uiArtifactsEnabled } = useUiArtifactPreferencesSnapshot() + const visibleMessages = React.useMemo( + () => uiArtifactsEnabled ? reconcileUiArtifactMessages(messages) : messages, + [messages, uiArtifactsEnabled], + ) const showLoading = shouldShowMessageListLoading(status, messages.length) - const items = React.useMemo(() => groupMessages(messages, status), [messages, status]); + const items = React.useMemo(() => groupMessages(visibleMessages, status), [visibleMessages, status]); const error = useSessionErrorMessage(); - const hasSessionErrorMessage = React.useMemo(() => messages.some(isSessionErrorMessage), [messages]) + const hasSessionErrorMessage = React.useMemo(() => visibleMessages.some(isSessionErrorMessage), [visibleMessages]) const liveActionLabel = isStreaming - ? getActiveToolLabel(collectToolParts(messages)) + ? getActiveToolLabel(collectToolParts(visibleMessages)) : null return (
- {messages.length === 0 && } + {visibleMessages.length === 0 && } {items.map((item) => { if (isMessageGroup(item)) { @@ -1162,15 +1193,15 @@ export function MessageList({ messages, status, retryStatus }: MessageListProps) ) } - const isLastMessage = item.index === messages.length - 1 + const isLastMessage = item.index === visibleMessages.length - 1 const isLastStep = - !messages[item.index + 1] || messages[item.index + 1].role !== item.message.role + !visibleMessages[item.index + 1] || visibleMessages[item.index + 1].role !== item.message.role return (
diff --git a/apps/app/src/components/tools/ui-artifact.tsx b/apps/app/src/components/tools/ui-artifact.tsx new file mode 100644 index 0000000000..bae2da81e4 --- /dev/null +++ b/apps/app/src/components/tools/ui-artifact.tsx @@ -0,0 +1,756 @@ +import type * as React from "react" +import { + BellRing, + CalendarDays, + CheckCircle2, + CircleAlert, + Clock3, + ExternalLink, + GraduationCap, + LayoutDashboard, + Mail, + MapPin, + MessageSquareText, + PanelsTopLeft, + ShieldAlert, + Target, + TrendingUp, + XCircle, +} from "lucide-react" +import type { + UiArtifactAction, + UiArtifactPayload, + UiArtifactRenderResult, + UiArtifactWidget, +} from "@openwork/types/ui-artifact" + +import { openDesktopUrl } from "@/app/lib/desktop" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +const INLINE_ROW_LIMIT = 5 + +function safeDate(value: string) { + const date = new Date(value) + return Number.isNaN(date.getTime()) ? null : date +} + +function formatTime(value: string, timezone?: string) { + const date = safeDate(value) + if (!date) return value + + try { + return new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "2-digit", + ...(timezone ? { timeZone: timezone } : {}), + }).format(date) + } catch { + return new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "2-digit", + }).format(date) + } +} + +function formatShortDateTime(value: string) { + const date = safeDate(value) + if (!date) return value + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }).format(date) +} + +function initials(value: string) { + return value + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toLocaleUpperCase()) + .join("") +} + +function isSafeWebUrl(value: string) { + try { + const url = new URL(value) + return url.protocol === "https:" && !url.username && !url.password + } catch { + return false + } +} + +function SourceBadge({ artifact }: { artifact: UiArtifactPayload }) { + const source = artifact.source + return ( +
+ {source.type === "mock" ? ( + + Demo data + + ) : ( + + Unverified source + + )} + + {source.provider ?? source.label} + {source.account ? ` · ${source.account}` : ""} + +
+ ) +} + +function ArtifactActionButton({ action }: { action: UiArtifactAction }) { + if (action.type !== "open_url" || !isSafeWebUrl(action.url)) return null + + return ( + + ) +} + +function artifactIcon(artifactId: UiArtifactPayload["artifactId"]) { + switch (artifactId) { + case "calendar.view": + return CalendarDays + case "widgets.collection": + return TrendingUp + case "workspace.brief": + return LayoutDashboard + case "communication.thread": + return MessageSquareText + case "mail.inbox": + return Mail + case "work.attention": + return BellRing + case "work.approvals": + return PanelsTopLeft + } +} + +function artifactIconClass(artifactId: UiArtifactPayload["artifactId"]) { + switch (artifactId) { + case "calendar.view": + return "bg-blue-3 text-blue-11" + case "widgets.collection": + return "bg-purple-3 text-purple-11" + case "workspace.brief": + return "bg-indigo-3 text-indigo-11" + case "communication.thread": + return "bg-violet-3 text-violet-11" + case "mail.inbox": + return "bg-orange-3 text-orange-11" + case "work.attention": + return "bg-red-3 text-red-11" + case "work.approvals": + return "bg-green-3 text-green-11" + } +} + +function ArtifactFrame(props: { + artifact: UiArtifactPayload + children: React.ReactNode + action?: Extract +}) { + const Icon = artifactIcon(props.artifact.artifactId) + const observedAt = props.artifact.source.observedAt + + return ( +
+
+
+
+
+

{props.artifact.title}

+ {props.artifact.subtitle ? ( +

{props.artifact.subtitle}

+ ) : null} +
+ + {props.artifact.presentation.size} + +
+ +
{props.children}
+ +
+ +
+ {observedAt ? ( + + ) : null} + {props.action ? : null} +
+
+
+ ) +} + +function RemainingRows({ count }: { count: number }) { + if (count <= 0) return null + return

+ {count} more

+} + +type CalendarArtifactPayload = Extract +type CalendarEvent = CalendarArtifactPayload["data"]["events"][number] + +function formatDateLabel(value: string, timezone: string, weekday: "short" | "long" = "short") { + const date = safeDate(value) + if (!date) return value + + try { + return new Intl.DateTimeFormat(undefined, { + weekday, + month: "short", + day: "numeric", + timeZone: timezone, + }).format(date) + } catch { + return value.slice(0, 10) + } +} + +function CalendarEventDetails(props: { + event: CalendarEvent + timezone: string + compact?: boolean +}) { + return ( +
+
+
+ + {props.event.title} + + {props.event.status === "tentative" ? Tentative : null} +
+
+ + {props.event.allDay + ? "All day" + : `${formatTime(props.event.start, props.timezone)}–${formatTime(props.event.end, props.timezone)}`} + + {props.event.location ? ( + + + ) : null} +
+
+ {props.event.action ? : null} +
+ ) +} + +function CalendarArtifact({ artifact }: { artifact: CalendarArtifactPayload }) { + const visibleEventLimit = artifact.data.variant === "day" ? INLINE_ROW_LIMIT : 8 + const events = artifact.data.events.slice(0, visibleEventLimit) + const groupedDates = [...new Set(events.map((event) => event.start.slice(0, 10)))] + + return ( + +
+ {artifact.data.variant} view + + {artifact.data.startDate === artifact.data.endDate + ? artifact.data.startDate + : `${artifact.data.startDate}–${artifact.data.endDate}`} + +
+ + {events.length === 0 ? ( +
+ + No events scheduled. +
+ ) : artifact.data.variant === "day" ? ( +
    + {events.map((event, index) => ( +
  1. + +
    + {index < events.length - 1 ? : null} + +
    + +
  2. + ))} +
+ ) : artifact.data.variant === "agenda" ? ( +
+ {groupedDates.map((date) => ( +
+

+ {formatDateLabel(`${date}T12:00:00Z`, artifact.data.timezone, "long")} +

+
    + {events.filter((event) => event.start.startsWith(date)).map((event) => ( +
  1. + +
  2. + ))} +
+
+ ))} +
+ ) : ( +
+ {groupedDates.map((date) => ( +
+

+ {formatDateLabel(`${date}T12:00:00Z`, artifact.data.timezone)} +

+
    + {events.filter((event) => event.start.startsWith(date)).map((event) => ( +
  1. + +
  2. + ))} +
+
+ ))} +
+ )} + + + + {artifact.data.variant === "day" && artifact.data.focusWindow ? ( +
+
+ ) : null} +
+ ) +} + +function CommunicationThreadArtifact({ artifact }: { artifact: Extract }) { + const messages = artifact.data.messages.slice(0, 4) + + return ( + + {artifact.data.topic ?

{artifact.data.topic}

: null} +
    + {messages.map((message) => ( +
  1. + +
    +
    + {message.author} + +
    +

    {message.body}

    + {message.reactions?.length ? ( +
    + {message.reactions.map((reaction) => ( + + {reaction.emoji} {reaction.count} + + ))} +
    + ) : null} +
    +
  2. + ))} +
+ +
+ ) +} + +function MailInboxArtifact({ artifact }: { artifact: Extract }) { + const messages = artifact.data.messages.slice(0, INLINE_ROW_LIMIT) + + return ( + +
+ {artifact.data.folder} + {artifact.data.unreadCount} unread +
+
    + {messages.map((message) => ( +
  1. + +
    +
    + + {message.sender} + + +
    +

    {message.subject}

    +

    {message.snippet}

    + {message.labels?.length ? ( +
    + {message.labels.map((label) => {label})} +
    + ) : null} +
    + {message.action ? : null} +
  2. + ))} +
+ +
+ ) +} + +function attentionIcon(kind: Extract["data"]["items"][number]["kind"]) { + switch (kind) { + case "incident": + return ShieldAlert + case "approval": + return CheckCircle2 + case "task": + return Clock3 + case "goal": + return Target + case "learning": + return GraduationCap + } +} + +function priorityClass(priority: Extract["data"]["items"][number]["priority"]) { + switch (priority) { + case "critical": + return "bg-red-3 text-red-11" + case "high": + return "bg-orange-3 text-orange-11" + case "normal": + return "bg-blue-3 text-blue-11" + case "low": + return "bg-muted text-muted-foreground" + } +} + +function AttentionArtifact({ artifact }: { artifact: Extract }) { + const items = artifact.data.items.slice(0, INLINE_ROW_LIMIT) + + return ( + +
    + {items.map((item) => { + const Icon = attentionIcon(item.kind) + return ( +
  1. + + +
    +
    + {item.title} + {item.priority === "critical" ? Critical : null} +
    + {item.description ?

    {item.description}

    : null} +
    + {item.source ? {item.source} : null} + {item.dueAt ? : null} +
    +
    + {item.action ? : null} +
  2. + ) + })} +
+ +
+ ) +} + +function WidgetsArtifact({ artifact }: { artifact: Extract }) { + const layoutClass = artifact.data.layout === "stack" + ? "grid-cols-1" + : artifact.data.layout === "strip" + ? "grid-cols-2 @lg/message-list:grid-cols-4" + : "grid-cols-1 @md/message-list:grid-cols-2" + + return ( + +
+ {artifact.data.widgets.map((widget) => ( +
+
+
+
+

{widget.label}

+ {widget.kind} +
+

+ {widget.value} + {widget.kind === "balance" && widget.unit ? ( + {widget.unit} + ) : null} +

+
+ {widget.action ? : null} +
+ {widget.detail ?

{widget.detail}

: null} + {widget.kind === "progress" ? ( +
+
+
+ ) : null} + {widget.kind === "metric" && widget.trend ? ( +

+ {widget.trend.direction === "up" ? "↗" : widget.trend.direction === "down" ? "↘" : "→"} {widget.trend.label} +

+ ) : null} + {widget.kind === "status" ? ( + + {widget.status} + + ) : null} + {widget.kind === "date" && widget.timestamp ? ( + + ) : null} +
+ ))} +
+ + ) +} + +function ApprovalArtifact(props: { + artifact: Extract + onRequestDecision?: (action: Extract) => void +}) { + return ( + +
    + {props.artifact.data.items.map((item) => ( +
  1. +
    + + {item.status === "rejected" ? : } + +
    +
    + {item.title} + + {item.status} + + {item.amount ? {item.amount} : null} +
    + {item.description ?

    {item.description}

    : null} +

    + {item.requestor} · {item.source} · submitted {formatShortDateTime(item.submittedAt)} +

    + {item.decisionNote ?

    “{item.decisionNote}”

    : null} +
    +
    + {item.status === "pending" && item.actions?.length && props.onRequestDecision ? ( +
    + {item.actions.map((action) => ( + + ))} +
    + ) : null} +
  2. + ))} +
+

+ {props.artifact.operation === "replace" ? "Updated mock state" : "Mock state"} · revision {props.artifact.revision} +

+
+ ) +} + +function WorkspaceBriefArtifact({ artifact }: { artifact: Extract }) { + const schedule = artifact.data.schedule.slice(0, 4) + const attention = artifact.data.attention.slice(0, 4) + + return ( + +

{artifact.data.summary}

+ +
+ {artifact.data.metrics.slice(0, 4).map((metric) => ( +
+
{metric.value}
+
{metric.label}
+
+ ))} +
+ +
+
+

+ + Today at a glance +

+
    + {schedule.map((event) => ( +
  1. + +
    +

    {event.title}

    + {event.location ?

    {event.location}

    : null} +
    +
  2. + ))} +
+
+ +
+

+ + Needs your attention +

+
    + {attention.map((item) => ( +
  1. + +
    +

    {item.title}

    +

    {item.source ?? item.kind}

    +
    +
  2. + ))} +
+
+
+ +
+

+ + Your widgets +

+
+ {artifact.data.progress.slice(0, 4).map((item) => ( +
+

{item.label}

+

{item.value}

+ {item.progress !== undefined ? ( +
+
+
+ ) : null} +
+ ))} +
+
+ + {artifact.data.quickActions.length > 0 ? ( +
+ {artifact.data.quickActions.map((action) => )} +
+ ) : null} +
+ ) +} + +function metricToneClass(tone: UiArtifactWidget["tone"]) { + switch (tone) { + case "info": + return "border-blue-6/30 bg-blue-3/30" + case "success": + return "border-green-6/30 bg-green-3/30" + case "warning": + return "border-orange-6/30 bg-orange-3/30" + case "critical": + return "border-red-6/30 bg-red-3/30" + case "neutral": + return "border-border bg-muted/30" + } +} + +export function UiArtifactCard({ + result, + onRequestDecision, +}: { + result: UiArtifactRenderResult + onRequestDecision?: (action: Extract) => void +}) { + const artifact = result.artifact + + switch (artifact.artifactId) { + case "workspace.brief": + return + case "calendar.view": + return + case "widgets.collection": + return + case "communication.thread": + return + case "mail.inbox": + return + case "work.attention": + return + case "work.approvals": + return + default: + return ( +
+ + UI artifact renderer unavailable. +
+ ) + } +} diff --git a/apps/app/src/i18n/locales/en.ts b/apps/app/src/i18n/locales/en.ts index a4334de7fd..e442a2dd17 100644 --- a/apps/app/src/i18n/locales/en.ts +++ b/apps/app/src/i18n/locales/en.ts @@ -1844,4 +1844,22 @@ export default { "session_management.empty_group": "No sessions", "session_management.ungrouped": "Ungrouped", "session_management.archived_label": "Archived", + "ui_artifacts.preferences_title": "UI artifacts", + "ui_artifacts.preferences_section_desc": "Chat-native cards that turn connected tool data into concise, visual answers.", + "ui_artifacts.preferences_toggle": "UI artifacts (Alpha)", + "ui_artifacts.preferences_toggle_desc": "Suggest and render native cards for successful OpenWork Cloud capability calls. Synced to your active organization membership.", + "ui_artifacts.kind.workspace_brief": "Workspace brief", + "ui_artifacts.kind.workspace_brief_desc": "A complete chat-native dashboard with your day, attention items, progress, and quick actions.", + "ui_artifacts.kind.calendar_view": "Calendar", + "ui_artifacts.kind.calendar_view_desc": "Day, chronological agenda, and date-grouped week variants.", + "ui_artifacts.kind.widgets_collection": "Widgets", + "ui_artifacts.kind.widgets_collection_desc": "Combine metric, progress, status, balance, and date widgets in one artifact.", + "ui_artifacts.kind.communication_thread": "Conversation thread", + "ui_artifacts.kind.communication_thread_desc": "Slack, Teams, or Google Chat message previews.", + "ui_artifacts.kind.mail_inbox": "Priority inbox", + "ui_artifacts.kind.mail_inbox_desc": "Gmail or Outlook messages likely to need attention.", + "ui_artifacts.kind.work_attention": "Attention queue", + "ui_artifacts.kind.work_attention_desc": "Incidents, approvals, tasks, goals, and learning items.", + "ui_artifacts.kind.work_approvals": "Approval queue", + "ui_artifacts.kind.work_approvals_desc": "Stateful mock approval and rejection decisions with revision checks.", } as const; diff --git a/apps/app/src/lib/ui-artifact-catalog.ts b/apps/app/src/lib/ui-artifact-catalog.ts new file mode 100644 index 0000000000..4b359cc378 --- /dev/null +++ b/apps/app/src/lib/ui-artifact-catalog.ts @@ -0,0 +1,61 @@ +import type { UiArtifactKind } from "@openwork/types/ui-artifact" + +export type StandardUiArtifactDefinition = { + artifactId: UiArtifactKind + labelKey: string + descriptionKey: string + category: "overview" | "time" | "communication" | "work" + sources: readonly string[] +} + +export const STANDARD_UI_ARTIFACTS = [ + { + artifactId: "workspace.brief", + labelKey: "ui_artifacts.kind.workspace_brief", + descriptionKey: "ui_artifacts.kind.workspace_brief_desc", + category: "overview", + sources: ["OpenWork Connect"], + }, + { + artifactId: "widgets.collection", + labelKey: "ui_artifacts.kind.widgets_collection", + descriptionKey: "ui_artifacts.kind.widgets_collection_desc", + category: "overview", + sources: ["Metrics", "Goals", "Payroll", "Service health"], + }, + { + artifactId: "calendar.view", + labelKey: "ui_artifacts.kind.calendar_view", + descriptionKey: "ui_artifacts.kind.calendar_view_desc", + category: "time", + sources: ["Google Calendar", "Outlook"], + }, + { + artifactId: "communication.thread", + labelKey: "ui_artifacts.kind.communication_thread", + descriptionKey: "ui_artifacts.kind.communication_thread_desc", + category: "communication", + sources: ["Slack", "Teams", "Google Chat"], + }, + { + artifactId: "mail.inbox", + labelKey: "ui_artifacts.kind.mail_inbox", + descriptionKey: "ui_artifacts.kind.mail_inbox_desc", + category: "communication", + sources: ["Gmail", "Outlook"], + }, + { + artifactId: "work.attention", + labelKey: "ui_artifacts.kind.work_attention", + descriptionKey: "ui_artifacts.kind.work_attention_desc", + category: "work", + sources: ["ServiceNow", "Workday", "Tasks"], + }, + { + artifactId: "work.approvals", + labelKey: "ui_artifacts.kind.work_approvals", + descriptionKey: "ui_artifacts.kind.work_approvals_desc", + category: "work", + sources: ["Workday", "ServiceNow"], + }, +] as const satisfies readonly StandardUiArtifactDefinition[] diff --git a/apps/app/src/lib/ui-artifacts.ts b/apps/app/src/lib/ui-artifacts.ts new file mode 100644 index 0000000000..e95c07e57a --- /dev/null +++ b/apps/app/src/lib/ui-artifacts.ts @@ -0,0 +1,160 @@ +import { + UI_ARTIFACT_RENDER_CAPABILITY, + UI_ARTIFACT_USE_CAPABILITY, + UI_ARTIFACT_MAX_JSON_BYTES, + uiArtifactRenderResultSchema, + type UiArtifactRenderResult, + type UiArtifactAction, +} from "@openwork/types/ui-artifact" +import type { UIMessage } from "ai" + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function parseJsonText(value: string): unknown { + if (new TextEncoder().encode(value).byteLength > UI_ARTIFACT_MAX_JSON_BYTES) { + return null + } + + try { + const parsed: unknown = JSON.parse(value) + return parsed + } catch { + return null + } +} + +function isWithinJsonLimit(value: unknown) { + try { + const serialized = typeof value === "string" ? value : JSON.stringify(value) + return typeof serialized === "string" + && new TextEncoder().encode(serialized).byteLength <= UI_ARTIFACT_MAX_JSON_BYTES + } catch { + return false + } +} + +function textContentPayload(value: unknown): unknown { + if (!isRecord(value) || !Array.isArray(value.content)) return null + + for (const item of value.content) { + if (!isRecord(item) || item.type !== "text" || typeof item.text !== "string") continue + const parsed = parseJsonText(item.text) + if (parsed !== null) return parsed + } + + return null +} + +export function isUiArtifactRenderToolName(toolName: string) { + const normalized = toolName.trim().toLocaleLowerCase("en-US") + return new Set([ + "render_artifact", + "use_artifact", + "ui-artifacts-demo_render_artifact", + "ui-artifacts-demo_use_artifact", + "openwork-ui-artifacts-demo_render_artifact", + "openwork-ui-artifacts-demo_use_artifact", + ]).has(normalized) +} + +export function isUiArtifactRenderInvocation(toolName: string, input: unknown) { + if (isUiArtifactRenderToolName(toolName)) return true + const normalized = toolName.trim().toLocaleLowerCase("en-US") + if (!new Set([ + "execute_capability", + "openwork-cloud_execute_capability", + ]).has(normalized)) { + return false + } + return isRecord(input) + && (input.name === UI_ARTIFACT_USE_CAPABILITY || input.name === UI_ARTIFACT_RENDER_CAPABILITY) +} + +export function buildUiArtifactDecisionPrompt( + action: Extract, +) { + const body = { + operation: "decide", + artifactId: "work.approvals", + instanceId: action.instanceId, + itemId: action.itemId, + decision: action.decision, + expectedRevision: action.expectedRevision, + } + const verb = action.decision === "approve" ? "Approve" : "Reject" + return [ + `${verb} the selected UI Artifacts mock approval.`, + `Search for and execute "${UI_ARTIFACT_USE_CAPABILITY}" with this minimal body: ${JSON.stringify(body)}.`, + "Mock only: Do not call a provider approval tool or infer any other decision.", + ].join(" ") +} + +export function parseUiArtifactRenderResult(value: unknown): UiArtifactRenderResult | null { + const candidates: unknown[] = [value] + + if (typeof value === "string") { + candidates.push(parseJsonText(value)) + } + + if (isRecord(value)) { + candidates.push(value.structuredContent, value.result, textContentPayload(value)) + if (typeof value.output === "string") { + candidates.push(parseJsonText(value.output)) + } + } + + for (const candidate of candidates) { + if (!isWithinJsonLimit(candidate)) continue + const parsed = uiArtifactRenderResultSchema.safeParse(candidate) + if (parsed.success) return parsed.data + } + + return null +} + +export function reconcileUiArtifactMessages(messages: UIMessage[]) { + const latest = new Map() + + messages.forEach((message, messageIndex) => { + message.parts.forEach((part, partIndex) => { + if ( + part.type !== "dynamic-tool" + || part.state !== "output-available" + || !isUiArtifactRenderInvocation(part.toolName, part.input) + ) { + return + } + const result = parseUiArtifactRenderResult(part.output) + if (!result) return + const key = result.artifact.instanceId + const current = latest.get(key) + if (!current || result.artifact.revision >= current.revision) { + latest.set(key, { messageIndex, partIndex, revision: result.artifact.revision }) + } + }) + }) + + let changed = false + const reconciled = messages.map((message, messageIndex) => { + const parts = message.parts.filter((part, partIndex) => { + if ( + part.type !== "dynamic-tool" + || part.state !== "output-available" + || !isUiArtifactRenderInvocation(part.toolName, part.input) + ) { + return true + } + const result = parseUiArtifactRenderResult(part.output) + if (!result) return true + const current = latest.get(result.artifact.instanceId) + const keep = current?.messageIndex === messageIndex && current.partIndex === partIndex + if (!keep) changed = true + return keep + }) + return parts.length === message.parts.length ? message : { ...message, parts } + }) + + return changed ? reconciled : messages +} diff --git a/apps/app/src/react-app/domains/session/chat/session-page.tsx b/apps/app/src/react-app/domains/session/chat/session-page.tsx index fe3bec5382..17d0033eeb 100644 --- a/apps/app/src/react-app/domains/session/chat/session-page.tsx +++ b/apps/app/src/react-app/domains/session/chat/session-page.tsx @@ -2,7 +2,7 @@ import type { CSSProperties } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { usePanelRef } from "react-resizable-panels"; -import { Cloud, FileText, Globe, Mic2, PanelRight, TextSearch, Zap } from "lucide-react"; +import { Cloud, FileText, Globe, LayoutDashboard, Mic2, PanelRight, TextSearch, Zap } from "lucide-react"; import { resolveExtensionIconSrc } from "@/react-app/design-system/extension-icon-src"; import { t } from "../../../../i18n"; @@ -68,6 +68,7 @@ import { isCollectibleArtifactTarget, isLocalhostBrowserTarget, isOpenableFileTa import type { OpenTargetOptions } from "@/lib/target-provider"; import { VoicePanel } from "../voice/voice-panel"; import { SidePanel } from "../panel/side-panel"; +import { UiArtifactCatalogPanel } from "../ui-artifacts/ui-artifact-catalog-panel"; import { getSidePanelSessionKey } from "../panel/side-panel-session"; import { TerminalDock } from "../terminal/terminal-dock"; import { useActivePanelTab, usePanelTabStore, useSessionPanelState } from "../panel/panel-tab-store"; @@ -75,6 +76,7 @@ import { useWorkspaceShellLayout } from "../../../shell/workspace-shell-layout"; import { useControlAction, type OpenworkControlAction } from "../../../shell/control/control-provider"; import { getExtensionId, isOpenWorkExtensionEnabled, OPENWORK_EXTENSION_STATE_CHANGED } from "../../settings/extension-state"; import { cn } from "@/lib/utils"; +import { useFeatureFlagsPreferences } from "../../settings/state/feature-flags-preferences"; import { canNavigateSelectedConversationHistory, createConversationTabHistory, @@ -313,6 +315,7 @@ function controlStringArg(args: unknown, key: string) { export function SessionPage(props: SessionPageProps) { const { config: shellConfig } = useShellConfig(); + const { uiArtifactsEnabled } = useFeatureFlagsPreferences(); const platform = usePlatform(); const denAuth = useDenAuth(); const sidebarOpen = useUiStateStore((state) => state.sidebarOpen); @@ -349,6 +352,7 @@ export function SessionPage(props: SessionPageProps) { const activeSidePanel = voiceSidePanelOpen ? "voice" : sessionSidePanel; const sidePanelOpen = activeSidePanel !== null; const panelRailActive = activeSidePanel === "panel"; + const uiArtifactsRailActive = activeSidePanel === "ui-artifacts"; const voiceRailActive = activeSidePanel === "voice"; const voiceExtension = useMemo( () => OPENWORK_EXTENSION_CATALOG.find((entry) => getExtensionId(entry) === "openwork-voice") ?? null, @@ -620,6 +624,9 @@ export function SessionPage(props: SessionPageProps) { toggleCurrentSidePanel("panel"); } }, [artifactFileTargets, hasArtifactTargets, openTab, panelRailActive, props.selectedSessionId, selectTab, sessionPanelState, setCurrentSidePanel, toggleCurrentSidePanel]); + const openUiArtifactsRailPane = useCallback(() => { + toggleCurrentSidePanel("ui-artifacts"); + }, [toggleCurrentSidePanel]); const openVoiceRailPane = useCallback(() => { toggleCurrentSidePanel("voice"); }, [toggleCurrentSidePanel]); @@ -671,6 +678,11 @@ export function SessionPage(props: SessionPageProps) { setCurrentSidePanel(null); } }, [activeSidePanel, setCurrentSidePanel, voiceExtensionEnabled]); + useEffect(() => { + if (activeSidePanel === "ui-artifacts" && !uiArtifactsEnabled) { + setCurrentSidePanel(null); + } + }, [activeSidePanel, setCurrentSidePanel, uiArtifactsEnabled]); const openVoicePanelControlAction = useMemo(() => ( voiceExtensionEnabled ? { @@ -1416,8 +1428,8 @@ export function SessionPage(props: SessionPageProps) { @@ -1426,6 +1438,8 @@ export function SessionPage(props: SessionPageProps) {
{props.settingsSlot}
+ ) : activeSidePanel === "ui-artifacts" ? ( + ) : activeSidePanel === "voice" ? ( ) : null} + {uiArtifactsEnabled ? ( + + ) : null}
diff --git a/apps/app/src/react-app/domains/session/surface/session-surface.tsx b/apps/app/src/react-app/domains/session/surface/session-surface.tsx index 2e12803e86..36b25bcbda 100644 --- a/apps/app/src/react-app/domains/session/surface/session-surface.tsx +++ b/apps/app/src/react-app/domains/session/surface/session-surface.tsx @@ -49,6 +49,11 @@ import { connectSkillPrompt, parseConnectSkillToken } from "./composer/connect-s import { createPastedTextChip, resolvePastedTextPlaceholders } from "./composer/pasted-text"; import { DevProfiler } from "@/react-app/shell/dev-profiler"; import { PaperGrainGradient } from "@openwork/ui/react"; +import { + UI_ARTIFACT_USE_CAPABILITY, + uiArtifactRenderResultSchema, + type UiArtifactRenderResult, +} from "@openwork/types/ui-artifact"; import { useShellConfig } from "@/react-app/shell/shell-config"; import { useReactRenderWatchdog } from "@/react-app/shell/react-render-watchdog"; import { SessionDebugPanel } from "./debug-panel"; @@ -155,6 +160,37 @@ function createMarkdownPrimitiveEvalMessages(sessionId: string) { return { messages, assistantMessageId }; } +function createUiArtifactEvalMessages( + sessionId: string, + result: UiArtifactRenderResult, +) { + const userMessageId = `${sessionId}:eval-ui-artifact-user`; + const assistantMessageId = `${sessionId}:eval-ui-artifact-assistant`; + const messages: UIMessage[] = [ + { + id: userMessageId, + role: "user", + parts: [{ type: "text", text: `Show ${result.artifact.title} as a native UI artifact.` }], + metadata: { opencode: { created: Date.now() } }, + }, + { + id: assistantMessageId, + role: "assistant", + parts: [{ + type: "dynamic-tool", + toolName: "openwork-cloud_execute_capability", + toolCallId: `${assistantMessageId}:render`, + state: "output-available", + input: { name: UI_ARTIFACT_USE_CAPABILITY }, + output: result, + }], + metadata: { opencode: { created: Date.now() + 1 } }, + }, + ]; + + return { messages, assistantMessageId }; +} + /** * Dev-only deterministic transcript exercising the Paper chat rules: * sentence-style capability calls, aggregated tool runs, collapsed @@ -850,6 +886,37 @@ export function SessionSurface(props: SessionSurfaceProps) { }; }, [props.sessionId]); useControlAction(props.isControlTarget ? seedMarkdownPrimitiveControlAction : null); + const seedUiArtifactControlAction = useMemo(() => { + if (!import.meta.env.DEV) return null; + + return { + id: "eval.ui_artifact.seed_chat", + label: "Seed UI artifact chat proof", + description: "Dev-only eval hook that renders a validated execute_capability artifact receipt.", + sideEffect: "mutation", + disabled: !props.sessionId, + execute: (args) => { + const controlArgs = args && typeof args === "object" + ? (args as { result?: unknown; clearPrompt?: unknown }) + : null; + const candidate = controlArgs && "result" in controlArgs + ? controlArgs.result + : args; + const result = uiArtifactRenderResultSchema.parse(candidate); + const seeded = createUiArtifactEvalMessages(props.sessionId, result); + setEvalMarkdownMessages(seeded.messages); + if (controlArgs?.clearPrompt === true) { + setComposerDraft(props.sessionId, ""); + } + return { + ok: true, + assistantMessageId: seeded.assistantMessageId, + artifactId: result.artifact.artifactId, + }; + }, + }; + }, [props.sessionId, setComposerDraft]); + useControlAction(props.isControlTarget ? seedUiArtifactControlAction : null); const seedChatTranscriptControlAction = useMemo(() => { if (!import.meta.env.DEV) return null; diff --git a/apps/app/src/react-app/domains/session/ui-artifacts/ui-artifact-catalog-panel.tsx b/apps/app/src/react-app/domains/session/ui-artifacts/ui-artifact-catalog-panel.tsx new file mode 100644 index 0000000000..98acaca720 --- /dev/null +++ b/apps/app/src/react-app/domains/session/ui-artifacts/ui-artifact-catalog-panel.tsx @@ -0,0 +1,170 @@ +import { + BellRing, + CalendarDays, + ClipboardCheck, + LayoutDashboard, + Mail, + MessageSquareText, + Sparkles, + TrendingUp, + X, +} from "lucide-react" +import type { UiArtifactKind } from "@openwork/types/ui-artifact" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Switch } from "@/components/ui/switch" +import { t } from "@/i18n" +import { STANDARD_UI_ARTIFACTS } from "@/lib/ui-artifact-catalog" +import { cn } from "@/lib/utils" +import { useFeatureFlagsPreferences } from "@/react-app/domains/settings/state/feature-flags-preferences" + +function artifactIcon(artifactId: UiArtifactKind) { + switch (artifactId) { + case "workspace.brief": + return LayoutDashboard + case "widgets.collection": + return TrendingUp + case "calendar.view": + return CalendarDays + case "communication.thread": + return MessageSquareText + case "mail.inbox": + return Mail + case "work.attention": + return BellRing + case "work.approvals": + return ClipboardCheck + } +} + +function artifactTone(artifactId: UiArtifactKind) { + switch (artifactId) { + case "workspace.brief": + return "bg-indigo-3 text-indigo-11" + case "widgets.collection": + return "bg-purple-3 text-purple-11" + case "calendar.view": + return "bg-blue-3 text-blue-11" + case "communication.thread": + return "bg-violet-3 text-violet-11" + case "mail.inbox": + return "bg-orange-3 text-orange-11" + case "work.attention": + return "bg-red-3 text-red-11" + case "work.approvals": + return "bg-green-3 text-green-11" + } +} + +function ArtifactCatalogTile(props: { + artifactId: UiArtifactKind + label: string + description: string + sources: readonly string[] + enabled: boolean + onToggle: () => void +}) { + const Icon = artifactIcon(props.artifactId) + + return ( +
+
+
+
+ +
+ +
+
+

{props.label}

+ {props.enabled ? ( + + Enabled + + ) : null} +
+

{props.description}

+
+ +
+ {props.sources.map((source) => ( + + {source} + + ))} +
+
+ ) +} + +export function UiArtifactCatalogPanel({ onClose }: { onClose: () => void }) { + const { + enabledUiArtifactIds, + toggleUiArtifact, + } = useFeatureFlagsPreferences() + const enabledCount = STANDARD_UI_ARTIFACTS.filter((definition) => ( + enabledUiArtifactIds.includes(definition.artifactId) + )).length + + return ( +
+
+
+
+
+
+

UI artifacts

+ Alpha +
+

{enabledCount} of {STANDARD_UI_ARTIFACTS.length} standard artifacts enabled

+
+ +
+ +
+
+
+
+
+ +
+ {STANDARD_UI_ARTIFACTS.map((definition) => ( + toggleUiArtifact(definition.artifactId)} + /> + ))} +
+
+
+ ) +} diff --git a/apps/app/src/react-app/domains/settings/pages/preferences-view.tsx b/apps/app/src/react-app/domains/settings/pages/preferences-view.tsx index 8f3c567f6d..3a2072d5cb 100644 --- a/apps/app/src/react-app/domains/settings/pages/preferences-view.tsx +++ b/apps/app/src/react-app/domains/settings/pages/preferences-view.tsx @@ -8,6 +8,10 @@ import { SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; +import { + type UiArtifactKind, +} from "@openwork/types/ui-artifact"; +import { STANDARD_UI_ARTIFACTS } from "@/lib/ui-artifact-catalog"; import { t } from "@/i18n"; import { @@ -42,6 +46,10 @@ export type PreferencesViewProps = { onDesktopNotificationsChange: (value: DesktopNotificationPreference) => void; memoryEnabled: boolean; onToggleMemory: () => void; + uiArtifactsEnabled: boolean; + enabledUiArtifactIds: UiArtifactKind[]; + onToggleUiArtifacts: () => void; + onToggleUiArtifact: (artifactId: UiArtifactKind) => void; }; function desktopNotificationPreferenceLabel(value: DesktopNotificationPreference) { @@ -188,6 +196,45 @@ export function PreferencesView(props: PreferencesViewProps) { + + + + {t("ui_artifacts.preferences_title")} + {t("ui_artifacts.preferences_section_desc")} + + + + + {t("ui_artifacts.preferences_toggle")} + {t("ui_artifacts.preferences_toggle_desc")} + + + + + + + {props.uiArtifactsEnabled ? STANDARD_UI_ARTIFACTS.map((definition) => ( + + + {t(definition.labelKey)} + {t(definition.descriptionKey)} + + props.onToggleUiArtifact(definition.artifactId)} + /> + + + + )) : null} + ); } diff --git a/apps/app/src/react-app/domains/settings/state/feature-flags-preferences.ts b/apps/app/src/react-app/domains/settings/state/feature-flags-preferences.ts index 63960a7638..17f94441b4 100644 --- a/apps/app/src/react-app/domains/settings/state/feature-flags-preferences.ts +++ b/apps/app/src/react-app/domains/settings/state/feature-flags-preferences.ts @@ -1,6 +1,67 @@ -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; +import { + UI_ARTIFACT_KINDS, + uiArtifactKindSchema, + type UiArtifactKind, + type UiArtifactPreferencesUpdate, +} from "@openwork/types/ui-artifact"; -import { useLocal } from "../../../kernel/local-provider"; +import { createDenClient, readDenSettings } from "../../../../app/lib/den"; +import { + denSessionUpdatedEvent, + denSettingsChangedEvent, +} from "../../../../app/lib/den-session-events"; +import { useLocal, useOptionalLocal } from "../../../kernel/local-provider"; + +let uiArtifactPreferenceSync: + | { key: string; promise: ReturnType["getUiArtifactPreferences"]> } + | null = null; + +function currentDenArtifactClient() { + const settings = readDenSettings(); + const token = settings.authToken?.trim() ?? ""; + const orgId = settings.activeOrgId?.trim() ?? ""; + if (!token || !orgId) return null; + return { + key: `${settings.baseUrl}::${orgId}::${token}`, + orgId, + client: createDenClient({ baseUrl: settings.baseUrl, token }), + }; +} + +function readCloudUiArtifactPreferences() { + const current = currentDenArtifactClient(); + if (!current) return null; + if (uiArtifactPreferenceSync?.key === current.key) return uiArtifactPreferenceSync.promise; + const promise = current.client.getUiArtifactPreferences(current.orgId); + uiArtifactPreferenceSync = { key: current.key, promise }; + void promise.catch(() => { + if (uiArtifactPreferenceSync?.promise === promise) uiArtifactPreferenceSync = null; + }); + return promise; +} + +async function writeCloudUiArtifactPreferences(update: UiArtifactPreferencesUpdate) { + const current = currentDenArtifactClient(); + if (!current) throw new Error("Sign in to OpenWork Cloud before enabling UI artifacts."); + const result = await current.client.updateUiArtifactPreferences(current.orgId, update); + uiArtifactPreferenceSync = { + key: current.key, + promise: Promise.resolve(result), + }; + return result; +} + +export function useUiArtifactPreferencesSnapshot() { + const local = useOptionalLocal(); + const uiArtifactsEnabled = local?.prefs.featureFlags?.uiArtifacts === true; + const enabledUiArtifactIds = (local?.prefs.uiArtifacts?.enabledArtifactIds ?? UI_ARTIFACT_KINDS) + .flatMap((value) => { + const parsed = uiArtifactKindSchema.safeParse(value); + return parsed.success ? [parsed.data] : []; + }); + return { uiArtifactsEnabled, enabledUiArtifactIds }; +} export function useFeatureFlagsPreferences() { const { prefs, setPrefs } = useLocal(); @@ -30,10 +91,121 @@ export function useFeatureFlagsPreferences() { })); }, [setPrefs]); + const { uiArtifactsEnabled, enabledUiArtifactIds } = useUiArtifactPreferencesSnapshot(); + + const applyCloudPreferences = useCallback((cloud: { + enabled: boolean; + enabledArtifactIds: UiArtifactKind[]; + }) => { + setPrefs((previous) => ({ + ...previous, + featureFlags: { + ...previous.featureFlags, + uiArtifacts: cloud.enabled, + }, + uiArtifacts: { + enabledArtifactIds: [...cloud.enabledArtifactIds], + }, + })); + }, [setPrefs]); + + useEffect(() => { + let disposed = false; + const sync = () => { + const request = readCloudUiArtifactPreferences(); + if (!request) { + applyCloudPreferences({ + enabled: false, + enabledArtifactIds: [...UI_ARTIFACT_KINDS], + }); + return; + } + void request.then((cloud) => { + if (!disposed) applyCloudPreferences(cloud); + }).catch(() => { + // Keep the last confirmed state during a temporary control-plane outage. + }); + }; + + sync(); + const handleSessionChange = () => { + uiArtifactPreferenceSync = null; + sync(); + }; + window.addEventListener(denSessionUpdatedEvent, handleSessionChange); + window.addEventListener(denSettingsChangedEvent, handleSessionChange); + return () => { + disposed = true; + window.removeEventListener(denSessionUpdatedEvent, handleSessionChange); + window.removeEventListener(denSettingsChangedEvent, handleSessionChange); + }; + }, [applyCloudPreferences]); + + const toggleUiArtifacts = useCallback(() => { + const nextEnabled = !uiArtifactsEnabled; + const update = { + enabled: nextEnabled, + enabledArtifactIds: enabledUiArtifactIds, + } satisfies UiArtifactPreferencesUpdate; + setPrefs((previous) => ({ + ...previous, + featureFlags: { + ...previous.featureFlags, + uiArtifacts: nextEnabled, + }, + })); + void writeCloudUiArtifactPreferences(update) + .then(applyCloudPreferences) + .catch(() => { + setPrefs((previous) => ({ + ...previous, + featureFlags: { + ...previous.featureFlags, + uiArtifacts: uiArtifactsEnabled, + }, + })); + }); + }, [ + applyCloudPreferences, + enabledUiArtifactIds, + setPrefs, + uiArtifactsEnabled, + ]); + + const toggleUiArtifact = useCallback((artifactId: UiArtifactKind) => { + const nextArtifactIds = enabledUiArtifactIds.includes(artifactId) + ? enabledUiArtifactIds.filter((value) => value !== artifactId) + : [...enabledUiArtifactIds, artifactId]; + setPrefs((previous) => ({ + ...previous, + uiArtifacts: { enabledArtifactIds: nextArtifactIds }, + })); + void writeCloudUiArtifactPreferences({ + enabled: uiArtifactsEnabled, + enabledArtifactIds: nextArtifactIds, + }) + .then(applyCloudPreferences) + .catch(() => { + setPrefs((previous) => ({ + ...previous, + uiArtifacts: { enabledArtifactIds: enabledUiArtifactIds }, + })); + }); + }, [ + applyCloudPreferences, + enabledUiArtifactIds, + setPrefs, + uiArtifactsEnabled, + ]); + return { microsandboxCreateSandboxEnabled, toggleMicrosandboxCreateSandbox, memoryEnabled, toggleMemory, + uiArtifactsEnabled, + enabledUiArtifactIds, + toggleUiArtifacts, + toggleUiArtifact, }; } diff --git a/apps/app/src/react-app/kernel/local-provider.tsx b/apps/app/src/react-app/kernel/local-provider.tsx index 515c3a40c2..1b10150dcb 100644 --- a/apps/app/src/react-app/kernel/local-provider.tsx +++ b/apps/app/src/react-app/kernel/local-provider.tsx @@ -18,6 +18,10 @@ import { isDesktopNotificationPreference, type DesktopNotificationPreference, } from "./desktop-notification-preferences"; +import { + UI_ARTIFACT_KINDS, + type UiArtifactKind, +} from "@openwork/types/ui-artifact"; import { LOCAL_PREFERENCES_KEY } from "./local-preferences-storage"; import { readStoredDefaultModel, @@ -54,6 +58,14 @@ export type LocalPreferences = { * stay callable (owner-scoped + authz'd). Off by default — opt-in preview. */ memory: boolean; + /** + * Chat-native UI artifact alpha. This local value is a render cache of the + * Den member preference; the capability executor reads the same cloud row. + */ + uiArtifacts: boolean; + }; + uiArtifacts: { + enabledArtifactIds: UiArtifactKind[]; }; /** * Set to true after the user completes the welcome/onboarding flow @@ -93,7 +105,8 @@ const INITIAL_PREFS: LocalPreferences = { defaultModel: null, selectedAgent: null, releaseChannel: "stable", - featureFlags: { microsandboxCreateSandbox: true, memory: false }, + featureFlags: { microsandboxCreateSandbox: true, memory: false, uiArtifacts: false }, + uiArtifacts: { enabledArtifactIds: [...UI_ARTIFACT_KINDS] }, hasCompletedOnboarding: false, analyticsEnabled: true, desktopNotifications: DEFAULT_DESKTOP_NOTIFICATION_PREFERENCE, @@ -225,8 +238,12 @@ export function LocalProvider({ children }: LocalProviderProps) { return {children}; } +export function useOptionalLocal(): LocalContextValue | undefined { + return use(LocalContext); +} + export function useLocal(): LocalContextValue { - const context = use(LocalContext); + const context = useOptionalLocal(); if (!context) { throw new Error("Local context is missing"); } diff --git a/apps/app/src/react-app/shell/settings-route.tsx b/apps/app/src/react-app/shell/settings-route.tsx index 5181fef4a4..c0d971644f 100644 --- a/apps/app/src/react-app/shell/settings-route.tsx +++ b/apps/app/src/react-app/shell/settings-route.tsx @@ -428,7 +428,14 @@ function SettingsRouteContent(props: SettingsSurfaceProps = {}) { const params = useParams<{ workspaceId?: string }>(); const routeWorkspaceId = props.workspaceId?.trim() || params.workspaceId?.trim() || ""; const local = useLocal(); - const { memoryEnabled, toggleMemory } = useFeatureFlagsPreferences(); + const { + memoryEnabled, + toggleMemory, + uiArtifactsEnabled, + enabledUiArtifactIds, + toggleUiArtifacts, + toggleUiArtifact, + } = useFeatureFlagsPreferences(); const platform = usePlatform(); const checkDesktopRestriction = useCheckDesktopRestriction(); const restrictionNotice = useRestrictionNotice(); @@ -2333,6 +2340,10 @@ function SettingsRouteContent(props: SettingsSurfaceProps = {}) { }} memoryEnabled={memoryEnabled} onToggleMemory={toggleMemory} + uiArtifactsEnabled={uiArtifactsEnabled} + enabledUiArtifactIds={enabledUiArtifactIds} + onToggleUiArtifacts={toggleUiArtifacts} + onToggleUiArtifact={toggleUiArtifact} /> ); case "extensions": diff --git a/apps/app/src/react-app/shell/ui-state-store.ts b/apps/app/src/react-app/shell/ui-state-store.ts index b428b827e5..2bc62974ec 100644 --- a/apps/app/src/react-app/shell/ui-state-store.ts +++ b/apps/app/src/react-app/shell/ui-state-store.ts @@ -14,7 +14,7 @@ export const DEFAULT_WORKSPACE_RIGHT_SIDEBAR_EXPANDED_WIDTH = 520; export const MIN_WORKSPACE_RIGHT_SIDEBAR_WIDTH = 320; export const MAX_WORKSPACE_RIGHT_SIDEBAR_WIDTH = 960; -export const SIDE_PANEL_ITEMS = ["panel", "extensions", "voice"] as const; +export const SIDE_PANEL_ITEMS = ["panel", "extensions", "ui-artifacts", "voice"] as const; export type SidePanelItem = (typeof SIDE_PANEL_ITEMS)[number]; export type SidePanelState = Record; diff --git a/apps/app/tests/openwork-context-projector.test.ts b/apps/app/tests/openwork-context-projector.test.ts index 5b84041715..9160c49023 100644 --- a/apps/app/tests/openwork-context-projector.test.ts +++ b/apps/app/tests/openwork-context-projector.test.ts @@ -73,6 +73,24 @@ describe("OpenWork context projector", () => { expect(context.sidePanel.tabs).toEqual([]); expect(context.sidePanel.activeTabId).toBeNull(); }); + + test("projects the UI artifact catalog as an agent-visible side panel", () => { + const context = buildOpenworkContext({ + ...baseInput, + ui: { + ...baseInput.ui, + sidePanelState: { "session-b": "ui-artifacts" }, + }, + }); + + expect(context.sidePanel).toEqual({ + open: true, + ownerSessionId: "session-b", + kind: "ui-artifacts", + tabs: [], + activeTabId: null, + }); + }); }); const splitWorkbench: WorkbenchSnapshot = { diff --git a/apps/app/tests/ui-artifacts.test.ts b/apps/app/tests/ui-artifacts.test.ts new file mode 100644 index 0000000000..7e94ca750b --- /dev/null +++ b/apps/app/tests/ui-artifacts.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, test } from "bun:test" +import type { UIMessage } from "ai" +import { + UI_ARTIFACT_RENDER_CAPABILITY, + UI_ARTIFACT_USE_CAPABILITY, + UI_ARTIFACT_PROTOCOL, + UI_ARTIFACT_SCHEMA_VERSION, + type UiArtifactRenderResult, +} from "@openwork/types/ui-artifact" +import { + buildUiArtifactDecisionPrompt, + isUiArtifactRenderInvocation, + isUiArtifactRenderToolName, + parseUiArtifactRenderResult, + reconcileUiArtifactMessages, +} from "@/lib/ui-artifacts" + +const RESULT = { + protocol: UI_ARTIFACT_PROTOCOL, + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + status: "rendered", + artifact: { + artifactId: "widgets.collection", + instanceId: "test-glance", + revision: 1, + operation: "create", + title: "Today", + presentation: { placement: "inline", size: "standard" }, + source: { type: "mock", label: "Test" }, + data: { + layout: "grid", + widgets: [ + { id: "meetings", kind: "metric", label: "Meetings", value: "2", tone: "info" }, + ], + }, + }, + narration: { + summary: "Rendered one metric.", + visibleFacts: ["Meetings: 2"], + }, +} satisfies UiArtifactRenderResult + +describe("UI artifact tool result parsing", () => { + test("recognizes direct and namespaced render tool names", () => { + expect(isUiArtifactRenderToolName("render_artifact")).toBe(true) + expect(isUiArtifactRenderToolName("use_artifact")).toBe(true) + expect(isUiArtifactRenderToolName("ui-artifacts-demo_render_artifact")).toBe(true) + expect(isUiArtifactRenderToolName("search_artifacts")).toBe(false) + expect(isUiArtifactRenderToolName("untrusted_render_artifact")).toBe(false) + expect(isUiArtifactRenderInvocation("openwork-cloud_execute_capability", { + name: UI_ARTIFACT_RENDER_CAPABILITY, + })).toBe(true) + expect(isUiArtifactRenderInvocation("openwork-cloud_execute_capability", { + name: UI_ARTIFACT_USE_CAPABILITY, + })).toBe(true) + expect(isUiArtifactRenderInvocation("openwork-cloud_execute_capability", { + name: "mcp:calendar:list_events", + })).toBe(false) + expect(isUiArtifactRenderInvocation("untrusted_execute_capability", { + name: UI_ARTIFACT_RENDER_CAPABILITY, + })).toBe(false) + }) + + test("parses direct, JSON text, and MCP structured content envelopes", () => { + expect(parseUiArtifactRenderResult(RESULT)?.artifact.instanceId).toBe("test-glance") + expect(parseUiArtifactRenderResult(JSON.stringify(RESULT))?.artifact.instanceId).toBe("test-glance") + expect(parseUiArtifactRenderResult({ structuredContent: RESULT })?.artifact.instanceId).toBe("test-glance") + }) + + test("fails closed for invalid envelopes", () => { + expect(parseUiArtifactRenderResult({ status: "rendered" })).toBeNull() + expect(parseUiArtifactRenderResult("not json")).toBeNull() + expect(parseUiArtifactRenderResult({ + ...RESULT, + artifact: { ...RESULT.artifact, operation: "replace" }, + })?.artifact.operation).toBe("replace") + expect(parseUiArtifactRenderResult({ + ...RESULT, + oversized: "x".repeat(40_001), + })).toBeNull() + }) + + test("builds a minimal explicit mock decision prompt", () => { + const prompt = buildUiArtifactDecisionPrompt({ + id: "approve-expense", + label: "Approve", + type: "request_decision", + instanceId: "demo-approval-queue", + itemId: "expense-lisbon", + decision: "approve", + expectedRevision: 3, + }) + expect(prompt).toContain(UI_ARTIFACT_USE_CAPABILITY) + expect(prompt).toContain('"expectedRevision":3') + expect(prompt).toContain("Do not call a provider approval tool") + expect(prompt).not.toContain("Customer workshop travel") + }) + + test("keeps only the newest native revision for an artifact instance", () => { + const replacement = { + ...RESULT, + artifact: { + ...RESULT.artifact, + revision: 2, + operation: "replace", + }, + } satisfies UiArtifactRenderResult + const messages = [ + { + id: "assistant-create", + role: "assistant", + parts: [{ + type: "dynamic-tool", + toolName: "openwork-cloud_execute_capability", + toolCallId: "create", + state: "output-available", + input: { name: UI_ARTIFACT_USE_CAPABILITY }, + output: RESULT, + }], + }, + { + id: "assistant-replace", + role: "assistant", + parts: [{ + type: "dynamic-tool", + toolName: "openwork-cloud_execute_capability", + toolCallId: "replace", + state: "output-available", + input: { name: UI_ARTIFACT_USE_CAPABILITY }, + output: replacement, + }], + }, + ] satisfies UIMessage[] + + const reconciled = reconcileUiArtifactMessages(messages) + expect(reconciled[0]?.parts).toHaveLength(0) + expect(reconciled[1]?.parts).toHaveLength(1) + const single = messages.slice(0, 1) + expect(reconcileUiArtifactMessages(single)).toBe(single) + }) +}) diff --git a/ee/apps/den-api/package.json b/ee/apps/den-api/package.json index 9603713bcc..606811bfe6 100644 --- a/ee/apps/den-api/package.json +++ b/ee/apps/den-api/package.json @@ -3,13 +3,14 @@ "private": true, "type": "module", "scripts": { - "dev": "pnpm run build:email && pnpm run build:install-config && pnpm run build:connect-link && pnpm run build:enterprise-mcp-client && OPENWORK_DEV_MODE=1 tsx watch src/main.ts", - "dev:local": "pnpm run build:email && pnpm run build:install-config && pnpm run build:connect-link && pnpm run build:enterprise-mcp-client && OPENWORK_DEV_MODE=1 PORT=${DEN_API_PORT:-8790} tsx watch src/main.ts", + "dev": "pnpm run build:email && pnpm run build:install-config && pnpm run build:connect-link && pnpm run build:enterprise-mcp-client && pnpm run build:ui-artifact-mcp && OPENWORK_DEV_MODE=1 tsx watch src/main.ts", + "dev:local": "pnpm run build:email && pnpm run build:install-config && pnpm run build:connect-link && pnpm run build:enterprise-mcp-client && pnpm run build:ui-artifact-mcp && OPENWORK_DEV_MODE=1 PORT=${DEN_API_PORT:-8790} tsx watch src/main.ts", "build": "node ./scripts/build.mjs", "build:email": "pnpm --filter @openwork/email build", "build:install-config": "pnpm --filter @openwork/install-config build", "build:connect-link": "pnpm --filter @openwork/connect-link build", "build:enterprise-mcp-client": "pnpm --filter @openwork/enterprise-mcp-client build", + "build:ui-artifact-mcp": "pnpm --filter @openwork/ui-artifact-mcp build", "build:den-db": "pnpm --filter @openwork-ee/den-db build", "openapi:snapshot": "tsx scripts/generate-openapi-snapshot.ts", "smoke:config-object-payload-boundary": "tsx scripts/smoke-config-object-payload-boundary.ts", @@ -48,6 +49,7 @@ "@openwork/enterprise-mcp-client": "workspace:*", "@openwork/install-config": "workspace:*", "@openwork/types": "workspace:*", + "@openwork/ui-artifact-mcp": "workspace:*", "@sentry/hono": "10.64.0", "@sentry/node": "10.64.0", "@standard-community/standard-json": "^0.3.5", diff --git a/ee/apps/den-api/scripts/build.mjs b/ee/apps/den-api/scripts/build.mjs index 4b2314e551..ec24048f43 100644 --- a/ee/apps/den-api/scripts/build.mjs +++ b/ee/apps/den-api/scripts/build.mjs @@ -113,6 +113,7 @@ function main() { run(pnpmCommand, ["run", "build:email"]) run(pnpmCommand, ["run", "build:install-config"]) run(pnpmCommand, ["run", "build:enterprise-mcp-client"]) + run(pnpmCommand, ["run", "build:ui-artifact-mcp"]) run(pnpmCommand, ["run", "build:den-db"]) run(pnpmCommand, ["exec", "tsc", "-p", "tsconfig.json"]) maybeUploadSentrySourcemaps() diff --git a/ee/apps/den-api/src/mcp/agent.ts b/ee/apps/den-api/src/mcp/agent.ts index 59773ed673..27b8012dbe 100644 --- a/ee/apps/den-api/src/mcp/agent.ts +++ b/ee/apps/den-api/src/mcp/agent.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto" import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { ErrorCode, McpError, type ToolAnnotations } from "@modelcontextprotocol/sdk/types.js" import { StreamableHTTPTransport } from "@hono/mcp" @@ -21,6 +22,18 @@ import { resolvePublicOrigin } from "../capability-sources/generic-oauth.js" import { env } from "../env.js" import { isPlatformAdminUserId } from "../middleware/admin.js" import { executeAvailableAdminCapability, parseAdminCapabilityName, searchAvailableAdminCapabilities } from "./admin-capabilities.js" +import { readUiArtifactPreferences } from "../ui-artifact-preferences.js" +import { + appendUiArtifactSuggestion, + executeUiArtifactCapability, + searchUiArtifactCapabilities, + suggestUiArtifactForCapability, +} from "./ui-artifacts.js" +import { + UI_ARTIFACT_KINDS, + UI_ARTIFACT_SCHEMA_VERSION, + type UiArtifactPreferences, +} from "@openwork/types/ui-artifact" import { executeBuiltinSkillCapability, listBuiltinSkillDescriptors, @@ -109,7 +122,7 @@ const externalCapabilityErrorPayloadSchema = z.object({ schemaGuidance: z.unknown().optional(), }) -export const AGENT_MCP_INSTRUCTIONS = [ +const BASE_AGENT_MCP_INSTRUCTIONS = [ "This OpenWork Cloud connection intentionally exposes exactly two tools: search_capabilities and execute_capability.", "Capabilities include native Google Workspace operations (Gmail read/search, Calendar list/create, Drive search/read, and Gmail draft creation) executed with the signed-in member's organization credentials, plus any MCP connections the organization has added.", "Allowlisted platform admins can also discover namespaced OpenWork Admin capabilities through this same connection; other members cannot discover or execute them.", @@ -125,7 +138,9 @@ export const AGENT_MCP_INSTRUCTIONS = [ "If the provider returns invalid_capability_arguments, correct the listed issues and retry once with changed arguments; never retry the same arguments unchanged. If it returns unknown_capability, call search_capabilities again before retrying.", "When a match has kind connection_status, name connectionStatus.connectionName and relay connectionStatus.action exactly. Distinguish the member's Your Connections page, the organization Connections dashboard, and the provider's own admin console.", "Connection probes are live. After the requested human fixes that connector, search again in the same task; otherwise do not retry unchanged or improvise workarounds through other tools.", -].join("\n") +] + +export const AGENT_MCP_INSTRUCTIONS = BASE_AGENT_MCP_INSTRUCTIONS.join("\n") async function mcpRequestMethod(request: Request): Promise { if (request.method.toUpperCase() !== "POST") return null @@ -167,6 +182,7 @@ const EXECUTE_CAPABILITY_TIMEOUT_MESSAGE = `The capability call exceeded ${EXECU export type ExecuteCapabilityToolResult = { isError?: boolean content: { text: string; type: "text" }[] + structuredContent?: Record } function textContent(text: string): { text: string; type: "text" }[] { @@ -380,6 +396,10 @@ export function registerAgentMcpRoutes a.name.localeCompare(b.name) || a.capability.localeCompare(b.capability)) } + const uiArtifactPreferences = method === "tools/call" && memberIdentity + ? await readUiArtifactPreferences(memberIdentity.orgMembershipId) + : { + protocol: "openwork.ui-artifact-preferences" as const, + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + enabled: false, + enabledArtifactIds: [...UI_ARTIFACT_KINDS], + updatedAt: null, + } satisfies UiArtifactPreferences const server = createAgentMcpServer() if (method === "initialize" || method === "resources/list" || method === "resources/read") { registerAgentSkillResources({ @@ -457,6 +486,9 @@ export function registerAgentMcpRoutes { - return executeCapabilityWithBudget({ + const result = await executeCapabilityWithBudget({ capability: name, invoke: async (): Promise => { + const uiArtifactResult = executeUiArtifactCapability({ + name, + body: normalizeToolBody(body), + preferences: uiArtifactPreferences, + stateScope: [ + memberIdentity?.orgMembershipId ?? `${principal.organizationId}:${principal.userId}`, + uiArtifactStateSession, + ].join(":"), + }) + if (uiArtifactResult) return uiArtifactResult + const adminResult = parseAdminCapabilityName(name) ? await executeAvailableAdminCapability(await resolvePlatformAdmin(), name, body) : null @@ -602,6 +645,16 @@ export function registerAgentMcpRoutes candidate.name === name) + return appendUiArtifactSuggestion(result, suggestUiArtifactForCapability({ + capability: name, + ...(operation?.operation.summary ? { title: operation.operation.summary } : {}), + ...(operation?.operation.description ? { description: operation.operation.description } : {}), + path, + query, + body: normalizeToolBody(body), + preferences: uiArtifactPreferences, + })) }, ) diff --git a/ee/apps/den-api/src/mcp/ui-artifacts.ts b/ee/apps/den-api/src/mcp/ui-artifacts.ts new file mode 100644 index 0000000000..c8ce01c079 --- /dev/null +++ b/ee/apps/den-api/src/mcp/ui-artifacts.ts @@ -0,0 +1,271 @@ +import { z } from "zod" +import { + UI_ARTIFACT_RENDER_CAPABILITY, + UI_ARTIFACT_SCHEMA_VERSION, + UI_ARTIFACT_SEARCH_CAPABILITY, + UI_ARTIFACT_USE_CAPABILITY, + uiArtifactErrorSchema, + uiArtifactRenderInputSchema, + uiArtifactRenderResultSchema, + uiArtifactSearchInputSchema, + uiArtifactSuggestionEnvelopeSchema, + uiArtifactUseInputSchema, + type UiArtifactErrorCode, + type UiArtifactPreferences, +} from "@openwork/types/ui-artifact" +import { + searchArtifacts, + UiArtifactMockStore, +} from "@openwork/ui-artifact-mcp" +import type { CapabilityMatch } from "./search.js" + +type CapabilityToolResult = { + isError?: boolean + content: { text: string; type: "text" }[] + structuredContent?: Record +} + +const managedSearchInputSchema = uiArtifactSearchInputSchema.omit({ + enabledArtifactIds: true, +}) + +const virtualCapabilityDefinitions = [ + { + name: UI_ARTIFACT_SEARCH_CAPABILITY, + summary: "Search the enabled OpenWork UI artifact catalog and return a strict use capability definition.", + bodySchema: z.toJSONSchema(managedSearchInputSchema), + keywords: ["artifact", "widget", "card", "render", "visual"], + }, + { + name: UI_ARTIFACT_USE_CAPABILITY, + summary: "Render an exact searched UI artifact or apply an explicit revision-safe decision to a mock approval.", + bodySchema: z.toJSONSchema(uiArtifactUseInputSchema), + keywords: ["artifact", "widget", "card", "render", "visual", "preview", "approval", "approve", "reject"], + }, +] as const + +function normalize(value: string) { + return value.trim().toLocaleLowerCase("en-US") +} + +function textContent(value: unknown) { + return [{ type: "text" as const, text: typeof value === "string" ? value : JSON.stringify(value) }] +} + +function virtualCapabilityMatch( + definition: typeof virtualCapabilityDefinitions[number], + query: string, +): CapabilityMatch { + const normalizedQuery = normalize(query) + const matchedKeywords = definition.keywords.filter((keyword) => normalizedQuery.includes(keyword)) + return { + name: definition.name, + method: "VIRTUAL", + path: "openwork://ui-artifacts", + score: Math.max(1, 80 + matchedKeywords.length * 10), + summary: definition.summary, + pathParams: [], + queryParams: [], + hasBody: true, + bodySchema: definition.bodySchema, + } +} + +export function searchUiArtifactCapabilities( + query: string, + limit: number, + preferences: UiArtifactPreferences, +): CapabilityMatch[] { + if (!preferences.enabled || preferences.enabledArtifactIds.length === 0) return [] + const normalizedQuery = normalize(query) + return virtualCapabilityDefinitions + .filter((definition) => ( + normalizedQuery.includes(normalize(definition.name)) + || definition.keywords.some((keyword) => normalizedQuery.includes(keyword)) + )) + .map((definition) => virtualCapabilityMatch(definition, query)) + .sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)) + .slice(0, Math.max(0, limit)) +} + +function artifactError(code: UiArtifactErrorCode, message: string): CapabilityToolResult { + const retry = (() => { + switch (code) { + case "schema_digest_mismatch": + case "manifest_changed": + case "unknown_artifact": + case "artifact_disabled": + return { action: "search_artifacts" as const, changedArgumentsRequired: true } + case "invalid_artifact_payload": + case "unsafe_action": + case "source_receipt_required": + case "source_receipt_invalid": + case "operation_unsupported": + case "revision_conflict": + case "state_not_found": + case "action_not_allowed": + return { action: "use_artifact" as const, changedArgumentsRequired: true } + default: + return { action: "none" as const, changedArgumentsRequired: false } + } + })() + const payload = uiArtifactErrorSchema.parse({ + protocol: "openwork.ui-artifact-error", + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + code, + message, + retry, + }) + return { isError: true, content: textContent(payload), structuredContent: payload } +} + +export function isUiArtifactCapability(name: string) { + return name === UI_ARTIFACT_SEARCH_CAPABILITY + || name === UI_ARTIFACT_USE_CAPABILITY + || name === UI_ARTIFACT_RENDER_CAPABILITY +} + +const MAX_SCOPED_MOCK_STORES = 250 +const scopedMockStores = new Map() + +function mockStoreFor(scope: string) { + const current = scopedMockStores.get(scope) + if (current) return current + if (scopedMockStores.size >= MAX_SCOPED_MOCK_STORES) { + const oldest = scopedMockStores.keys().next().value + if (typeof oldest === "string") scopedMockStores.delete(oldest) + } + const created = new UiArtifactMockStore() + scopedMockStores.set(scope, created) + return created +} + +export function executeUiArtifactCapability(input: { + name: string + body: unknown + preferences: UiArtifactPreferences + stateScope: string +}): CapabilityToolResult | null { + if (!isUiArtifactCapability(input.name)) return null + if (!input.preferences.enabled) { + return artifactError("artifact_disabled", "UI artifacts are disabled for this OpenWork member.") + } + + if (input.name === UI_ARTIFACT_SEARCH_CAPABILITY) { + const parsed = managedSearchInputSchema.safeParse(input.body) + if (!parsed.success) { + return artifactError("invalid_search_input", "The UI artifact search input is invalid.") + } + const result = searchArtifacts({ + ...parsed.data, + enabledArtifactIds: input.preferences.enabledArtifactIds, + }, { transport: "execute_capability" }) + return { content: textContent(result), structuredContent: result } + } + + const parsed = input.name === UI_ARTIFACT_RENDER_CAPABILITY + ? uiArtifactRenderInputSchema.safeParse(input.body) + : uiArtifactUseInputSchema.safeParse(input.body) + if (!parsed.success) { + return artifactError("invalid_artifact_payload", "The UI artifact use input is invalid.") + } + const artifactId = "artifactId" in parsed.data ? parsed.data.artifactId : "work.approvals" + if (!input.preferences.enabledArtifactIds.includes(artifactId)) { + return artifactError("artifact_disabled", `${artifactId} is disabled for this OpenWork member.`) + } + const resolved = mockStoreFor(input.stateScope).use(parsed.data) + if (!resolved.ok) return artifactError(resolved.code, resolved.message) + const result = uiArtifactRenderResultSchema.parse(resolved.result) + return { content: textContent(result), structuredContent: result } +} + +function presentArgumentKeys(value: unknown): Record | undefined { + if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined + const keys = Object.keys(value).slice(0, 40) + return Object.fromEntries(keys.map((key) => [key, ""])) +} + +export function suggestUiArtifactForCapability(input: { + capability: string + title?: string + description?: string + path?: unknown + query?: unknown + body?: unknown + preferences: UiArtifactPreferences +}) { + if (!input.preferences.enabled || isUiArtifactCapability(input.capability)) return null + const argumentsShape = { + ...(presentArgumentKeys(input.path) ? { path: presentArgumentKeys(input.path) } : {}), + ...(presentArgumentKeys(input.query) ? { query: presentArgumentKeys(input.query) } : {}), + ...(presentArgumentKeys(input.body) ? { body: presentArgumentKeys(input.body) } : {}), + } + const result = searchArtifacts({ + query: `Native UI for ${input.capability}`, + signal: { + toolName: input.capability, + ...(input.title ? { toolTitle: input.title } : {}), + ...(input.description ? { toolDescription: input.description } : {}), + ...(Object.keys(argumentsShape).length ? { arguments: argumentsShape } : {}), + }, + enabledArtifactIds: input.preferences.enabledArtifactIds, + limit: 1, + }, { transport: "execute_capability" }) + const match = result.matches[0] + if (!match) return null + + return uiArtifactSuggestionEnvelopeSchema.parse({ + protocol: "openwork.ui-artifact-suggestions", + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + agentInstruction: [ + "This optional UI enhancement expires at the end of the current turn.", + "Render at most one suggestion and skip duplicate dedupeKey values.", + "If it materially improves the answer, follow the returned invocation once, then use the exact searched schema digest and example body.", + "The alpha example is mock data: never replace its payload with provider values or imply it is live.", + "After rendering, use narration.summary and only decision-relevant visibleFacts; never infer or execute an approval decision without the user's explicit choice.", + ].join(" "), + trigger: { capability: input.capability }, + contextPolicy: { + selection: "optional", + maxRendersThisTurn: 1, + expires: "end_of_turn", + dedupeKey: `${input.capability}:${match.artifactId}`.slice(0, 160), + includesSourceValues: false, + }, + suggestions: [{ + artifactId: match.artifactId, + title: match.title, + reason: match.reasons.join("; ").slice(0, 300) || `Matched ${input.capability}`, + invocation: { + toolName: "execute_capability", + arguments: { + name: UI_ARTIFACT_SEARCH_CAPABILITY, + body: { + query: `Render the best native artifact for ${input.capability}`, + signal: { + toolName: input.capability, + ...(input.title ? { toolTitle: input.title } : {}), + ...(input.description ? { toolDescription: input.description } : {}), + ...(Object.keys(argumentsShape).length ? { arguments: argumentsShape } : {}), + }, + limit: 1, + }, + }, + }, + }], + }) +} + +export function appendUiArtifactSuggestion( + result: CapabilityToolResult, + suggestion: ReturnType, +): CapabilityToolResult { + if (result.isError || !suggestion) return result + return { + ...result, + content: [ + ...result.content, + ...textContent({ uiArtifactSuggestions: suggestion }), + ], + } +} diff --git a/ee/apps/den-api/src/routes/me/index.ts b/ee/apps/den-api/src/routes/me/index.ts index 840cdf9606..8733de5d0b 100644 --- a/ee/apps/den-api/src/routes/me/index.ts +++ b/ee/apps/den-api/src/routes/me/index.ts @@ -2,6 +2,10 @@ import { eq } from "@openwork-ee/den-db/drizzle" import { AuthAccountTable, AuthUserTable, RateLimitTable } from "@openwork-ee/den-db/schema" import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid" import { desktopConfigSchema } from "@openwork/types/den/desktop-policies" +import { + uiArtifactPreferencesSchema, + uiArtifactPreferencesUpdateSchema, +} from "@openwork/types/ui-artifact" import type { Hono } from "hono" import { describeRoute } from "hono-openapi" import { z } from "zod" @@ -16,6 +20,10 @@ import type { AuthContextVariables } from "../../session.js" import { calculateDesktopPolicyForOrgMember } from "../../desktop-policies.js" import { memberFacingMcpConnectionsEnabled } from "../../capability-sources/external-mcp-rollout.js" import { DenEmailSendError, sendEmail } from "../../utils/email/send-email.js" +import { + readUiArtifactPreferences, + writeUiArtifactPreferences, +} from "../../ui-artifact-preferences.js" const DOWNLOAD_LINK_RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000 const DOWNLOAD_LINK_RATE_LIMIT_MAX = 5 @@ -38,6 +46,10 @@ const meDesktopConfigResponseSchema = desktopConfigSchema.meta({ ref: "CurrentUserDesktopConfigResponse", }) +const meUiArtifactPreferencesResponseSchema = uiArtifactPreferencesSchema.meta({ + ref: "CurrentUserUiArtifactPreferencesResponse", +}) + const sendDownloadLinkResponseSchema = z.object({ ok: z.literal(true), }).meta({ ref: "SendDownloadLinkResponse" }) @@ -395,4 +407,42 @@ export function registerMeRoutes { + const currentMember = c.get("organizationContext").currentMember + return c.json(await readUiArtifactPreferences(currentMember.id)) + }, + ) + + app.put( + "/v1/me/ui-artifacts", + describeRoute({ + tags: ["Users"], + summary: "Update current user's UI artifact preferences", + description: "Updates the UI artifact alpha state used by the desktop renderer and the OpenWork Cloud capability executor.", + responses: { + 200: jsonResponse("UI artifact preferences updated successfully.", meUiArtifactPreferencesResponseSchema), + 400: jsonResponse("The UI artifact preference update was invalid.", invalidRequestSchema), + 401: jsonResponse("The caller must be signed in to update UI artifact preferences.", unauthorizedSchema), + }, + }), + orgMemberRoute(), + jsonValidator(uiArtifactPreferencesUpdateSchema), + async (c) => { + const currentMember = c.get("organizationContext").currentMember + return c.json(await writeUiArtifactPreferences(currentMember.id, c.req.valid("json"))) + }, + ) } diff --git a/ee/apps/den-api/src/ui-artifact-preferences.ts b/ee/apps/den-api/src/ui-artifact-preferences.ts new file mode 100644 index 0000000000..aef22e4f84 --- /dev/null +++ b/ee/apps/den-api/src/ui-artifact-preferences.ts @@ -0,0 +1,96 @@ +import { eq } from "@openwork-ee/den-db/drizzle" +import { UiArtifactPreferenceTable } from "@openwork-ee/den-db/schema" +import { normalizeDenTypeId } from "@openwork-ee/utils/typeid" +import { + UI_ARTIFACT_KINDS, + UI_ARTIFACT_SCHEMA_VERSION, + uiArtifactPreferencesUpdateSchema, + type UiArtifactPreferences, + type UiArtifactPreferencesUpdate, +} from "@openwork/types/ui-artifact" +import { db } from "./db.js" + +const DEFAULT_UI_ARTIFACT_PREFERENCES: UiArtifactPreferences = { + protocol: "openwork.ui-artifact-preferences", + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + enabled: false, + enabledArtifactIds: [...UI_ARTIFACT_KINDS], + updatedAt: null, +} + +function normalizedUpdate(value: unknown): UiArtifactPreferencesUpdate { + const parsed = uiArtifactPreferencesUpdateSchema.parse(value) + return { + enabled: parsed.enabled, + enabledArtifactIds: UI_ARTIFACT_KINDS.filter((artifactId) => ( + parsed.enabledArtifactIds.includes(artifactId) + )), + } +} + +function normalizedStoredArtifactIds(value: unknown): UiArtifactPreferences["enabledArtifactIds"] { + const stored = Array.isArray(value) ? value : [] + return UI_ARTIFACT_KINDS.filter((artifactId) => ( + stored.includes(artifactId) + || (artifactId === "calendar.view" && stored.includes("calendar.day")) + || ( + artifactId === "widgets.collection" + && (stored.includes("metrics.glance") || stored.includes("work.progress")) + ) + )) +} + +export async function readUiArtifactPreferences(memberId: string): Promise { + const normalizedMemberId = normalizeDenTypeId("member", memberId) + const rows = await db + .select({ + enabled: UiArtifactPreferenceTable.enabled, + enabledArtifactIds: UiArtifactPreferenceTable.enabledArtifactIds, + updatedAt: UiArtifactPreferenceTable.updatedAt, + }) + .from(UiArtifactPreferenceTable) + .where(eq(UiArtifactPreferenceTable.memberId, normalizedMemberId)) + .limit(1) + const row = rows[0] + if (!row) return DEFAULT_UI_ARTIFACT_PREFERENCES + + const enabledArtifactIds = normalizedStoredArtifactIds(row.enabledArtifactIds) + return { + protocol: "openwork.ui-artifact-preferences", + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + enabled: row.enabled === true, + enabledArtifactIds, + updatedAt: row.updatedAt.toISOString(), + } +} + +export async function writeUiArtifactPreferences( + memberId: string, + value: unknown, +): Promise { + const normalizedMemberId = normalizeDenTypeId("member", memberId) + const update = normalizedUpdate(value) + const updatedAt = new Date() + await db + .insert(UiArtifactPreferenceTable) + .values({ + memberId: normalizedMemberId, + enabled: update.enabled, + enabledArtifactIds: update.enabledArtifactIds, + updatedAt, + }) + .onDuplicateKeyUpdate({ + set: { + enabled: update.enabled, + enabledArtifactIds: update.enabledArtifactIds, + updatedAt, + }, + }) + + return { + protocol: "openwork.ui-artifact-preferences", + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + ...update, + updatedAt: updatedAt.toISOString(), + } +} diff --git a/ee/apps/den-api/test/mcp-agent-timeouts.test.ts b/ee/apps/den-api/test/mcp-agent-timeouts.test.ts index c8be9a2ebd..e2fa12d5b6 100644 --- a/ee/apps/den-api/test/mcp-agent-timeouts.test.ts +++ b/ee/apps/den-api/test/mcp-agent-timeouts.test.ts @@ -106,7 +106,7 @@ test("executeCapabilityWithBudget swallows late rejections after timeout", async } }) -test("agent MCP server exposes steering instructions during initialize", async () => { +test("agent MCP server keeps UI artifact steering out of the default initialize prompt", async () => { const server = agentModule.createAgentMcpServer() const client = new Client({ name: "test-client", version: "1.0.0" }) const transports = createMemoryTransportPair() @@ -130,6 +130,9 @@ test("agent MCP server exposes steering instructions during initialize", async ( expect(client.getInstructions()).toContain("connectionStatus.connectionName") expect(client.getInstructions()).toContain("schemaGuidance is advisory") expect(client.getInstructions()).toContain("always attempts the downstream provider call") + expect(client.getInstructions()).not.toContain("UI Artifacts") + expect(client.getInstructions()).not.toContain("uiArtifactSuggestions") + expect(client.getInstructions()).not.toContain("openwork.ui_artifacts") expect(client.getInstructions()).toContain("invalid_capability_arguments") expect(client.getInstructions()).toContain("never retry the same arguments unchanged") diff --git a/ee/apps/den-api/test/ui-artifacts-capability.test.ts b/ee/apps/den-api/test/ui-artifacts-capability.test.ts new file mode 100644 index 0000000000..269b2da46f --- /dev/null +++ b/ee/apps/den-api/test/ui-artifacts-capability.test.ts @@ -0,0 +1,181 @@ +import { describe, expect, test } from "bun:test" +import { + UI_ARTIFACT_KINDS, + UI_ARTIFACT_SEARCH_CAPABILITY, + UI_ARTIFACT_USE_CAPABILITY, + uiArtifactRenderResultSchema, + uiArtifactSearchResultSchema, + uiArtifactSuggestionEnvelopeSchema, + type UiArtifactPreferences, +} from "@openwork/types/ui-artifact" +import { + appendUiArtifactSuggestion, + executeUiArtifactCapability, + searchUiArtifactCapabilities, + suggestUiArtifactForCapability, +} from "../src/mcp/ui-artifacts.js" + +const ENABLED_PREFERENCES = { + protocol: "openwork.ui-artifact-preferences", + schemaVersion: "1", + enabled: true, + enabledArtifactIds: [...UI_ARTIFACT_KINDS], + updatedAt: "2026-07-23T10:00:00.000Z", +} satisfies UiArtifactPreferences + +describe("execute_capability UI artifact adapter", () => { + test("discovers virtual search and use capabilities only when enabled", () => { + const matches = searchUiArtifactCapabilities("artifact widget", 5, ENABLED_PREFERENCES) + expect(matches.map((match) => match.name)).toEqual([ + UI_ARTIFACT_SEARCH_CAPABILITY, + UI_ARTIFACT_USE_CAPABILITY, + ]) + expect(searchUiArtifactCapabilities("artifact widget", 5, { + ...ENABLED_PREFERENCES, + enabled: false, + })).toEqual([]) + }) + + test("does not enrich ordinary capability results when disabled", () => { + const disabledPreferences = { + ...ENABLED_PREFERENCES, + enabled: false, + } + const suggestion = suggestUiArtifactForCapability({ + capability: "mcp:google-calendar:list_events", + title: "List calendar events", + body: { calendarId: "private-calendar-id" }, + preferences: disabledPreferences, + }) + const original = { + content: [{ type: "text" as const, text: "{\"events\":[]}" }], + } + + expect(suggestion).toBeNull() + expect(appendUiArtifactSuggestion(original, suggestion)).toBe(original) + expect(executeUiArtifactCapability({ + name: UI_ARTIFACT_SEARCH_CAPABILITY, + body: { query: "calendar events today" }, + preferences: disabledPreferences, + stateScope: "member-disabled", + })?.structuredContent).toMatchObject({ code: "artifact_disabled" }) + }) + + test("searches and renders through virtual execute_capability names", () => { + const searched = executeUiArtifactCapability({ + name: UI_ARTIFACT_SEARCH_CAPABILITY, + body: { + query: "calendar events today", + signal: { toolName: "google_calendar_list_events" }, + limit: 1, + }, + preferences: ENABLED_PREFERENCES, + stateScope: "member-calendar", + }) + expect(searched?.isError).not.toBe(true) + const searchResult = uiArtifactSearchResultSchema.parse(searched?.structuredContent) + expect(searchResult.matches[0]?.artifactId).toBe("calendar.view") + expect(searchResult.matches[0]?.toolDefinition.invocation).toEqual({ + toolName: "execute_capability", + capability: UI_ARTIFACT_USE_CAPABILITY, + argumentsField: "body", + }) + + const rendered = executeUiArtifactCapability({ + name: UI_ARTIFACT_USE_CAPABILITY, + body: searchResult.matches[0]?.toolDefinition.exampleArguments, + preferences: ENABLED_PREFERENCES, + stateScope: "member-calendar", + }) + expect(rendered?.isError).not.toBe(true) + const renderResult = uiArtifactRenderResultSchema.parse(rendered?.structuredContent) + expect(renderResult.artifact.artifactId).toBe("calendar.view") + expect(renderResult.status).toBe("rendered") + }) + + test("adds a bounded suggestion only after a matching ordinary capability", () => { + const suggestion = suggestUiArtifactForCapability({ + capability: "mcp:google-calendar:list_events", + title: "List calendar events", + body: { + calendarId: "private-calendar-id", + accessToken: "must-not-be-copied", + }, + preferences: ENABLED_PREFERENCES, + }) + const parsed = uiArtifactSuggestionEnvelopeSchema.parse(suggestion) + expect(parsed.suggestions[0]?.artifactId).toBe("calendar.view") + expect(parsed.agentInstruction).toContain("expires at the end of the current turn") + expect(parsed.agentInstruction).toContain("Render at most one suggestion") + expect(parsed.agentInstruction).toContain("never replace its payload with provider values") + expect(parsed.agentInstruction).toContain("never infer or execute an approval decision") + expect(parsed.contextPolicy).toEqual({ + selection: "optional", + maxRendersThisTurn: 1, + expires: "end_of_turn", + dedupeKey: "mcp:google-calendar:list_events:calendar.view", + includesSourceValues: false, + }) + const serialized = JSON.stringify(parsed) + expect(serialized).not.toContain("private-calendar-id") + expect(serialized).not.toContain("must-not-be-copied") + + const enriched = appendUiArtifactSuggestion({ + content: [{ type: "text", text: "{\"events\":[]}" }], + }, parsed) + expect(enriched.content).toHaveLength(2) + expect(enriched.content[1]?.text).toContain("uiArtifactSuggestions") + }) + + test("holds isolated approval state and rejects stale revisions", () => { + const searched = executeUiArtifactCapability({ + name: UI_ARTIFACT_SEARCH_CAPABILITY, + body: { query: "approval requests", limit: 1 }, + preferences: ENABLED_PREFERENCES, + stateScope: "member-approvals", + }) + const searchResult = uiArtifactSearchResultSchema.parse(searched?.structuredContent) + expect(searchResult.matches[0]?.artifactId).toBe("work.approvals") + + const rendered = executeUiArtifactCapability({ + name: UI_ARTIFACT_USE_CAPABILITY, + body: searchResult.matches[0]?.toolDefinition.exampleArguments, + preferences: ENABLED_PREFERENCES, + stateScope: "member-approvals", + }) + const initial = uiArtifactRenderResultSchema.parse(rendered?.structuredContent) + + const decided = executeUiArtifactCapability({ + name: UI_ARTIFACT_USE_CAPABILITY, + body: { + operation: "decide", + artifactId: "work.approvals", + instanceId: initial.artifact.instanceId, + itemId: "expense-lisbon", + decision: "reject", + expectedRevision: 1, + }, + preferences: ENABLED_PREFERENCES, + stateScope: "member-approvals", + }) + const updated = uiArtifactRenderResultSchema.parse(decided?.structuredContent) + expect(updated.artifact.revision).toBe(2) + expect(updated.interaction?.decision).toBe("reject") + + const stale = executeUiArtifactCapability({ + name: UI_ARTIFACT_USE_CAPABILITY, + body: { + operation: "decide", + artifactId: "work.approvals", + instanceId: initial.artifact.instanceId, + itemId: "access-production", + decision: "approve", + expectedRevision: 1, + }, + preferences: ENABLED_PREFERENCES, + stateScope: "member-approvals", + }) + expect(stale?.isError).toBe(true) + expect(stale?.structuredContent).toMatchObject({ code: "revision_conflict" }) + }) +}) diff --git a/ee/packages/den-db/drizzle/0050_oval_longshot.sql b/ee/packages/den-db/drizzle/0050_oval_longshot.sql new file mode 100644 index 0000000000..2db01a0bb6 --- /dev/null +++ b/ee/packages/den-db/drizzle/0050_oval_longshot.sql @@ -0,0 +1,7 @@ +CREATE TABLE `ui_artifact_preference` ( + `member_id` varchar(64) NOT NULL, + `enabled` boolean NOT NULL DEFAULT false, + `enabled_artifact_ids` json NOT NULL, + `updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + CONSTRAINT `ui_artifact_preference_member_id` PRIMARY KEY(`member_id`) +); diff --git a/ee/packages/den-db/drizzle/meta/0048_snapshot.json b/ee/packages/den-db/drizzle/meta/0048_snapshot.json index 3d91096bf3..0ef2a8dd79 100644 --- a/ee/packages/den-db/drizzle/meta/0048_snapshot.json +++ b/ee/packages/den-db/drizzle/meta/0048_snapshot.json @@ -9276,4 +9276,4 @@ } } } -} \ No newline at end of file +} diff --git a/ee/packages/den-db/drizzle/meta/0050_snapshot.json b/ee/packages/den-db/drizzle/meta/0050_snapshot.json new file mode 100644 index 0000000000..55e234b065 --- /dev/null +++ b/ee/packages/den-db/drizzle/meta/0050_snapshot.json @@ -0,0 +1,9333 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "c0075abb-fd60-4819-8e19-6b35eef4ea65", + "prevId": "a4badcd3-f08e-48fc-beb1-2c72b5812e8e", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "account_user_id": { + "name": "account_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "account_id": { + "name": "account_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refill_interval": { + "name": "refill_interval", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "apikey_config_id": { + "name": "apikey_config_id", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "apikey_reference_id": { + "name": "apikey_reference_id", + "columns": [ + "reference_id" + ], + "isUnique": false + }, + "apikey_key": { + "name": "apikey_key", + "columns": [ + "key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "apikey_id": { + "name": "apikey_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "jwks": { + "name": "jwks", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "alg": { + "name": "alg", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crv": { + "name": "crv", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "jwks_id": { + "name": "jwks_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_team_id": { + "name": "active_team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "session_token": { + "name": "session_token", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_user_id": { + "name": "session_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_id": { + "name": "session_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "user_email": { + "name": "user_email", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_created_at_id": { + "name": "user_created_at_id", + "columns": [ + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "verification_identifier": { + "name": "verification_identifier", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_id": { + "name": "verification_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "external_identity": { + "name": "external_identity", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scim_provider_id": { + "name": "scim_provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_id": { + "name": "remote_id", + "type": "varchar(191)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(191)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "varchar(191)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(191)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(191)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_json": { + "name": "name_json", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emails_json": { + "name": "emails_json", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attributes_json": { + "name": "attributes_json", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_scim_sync_at": { + "name": "last_scim_sync_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sso_login_at": { + "name": "last_sso_login_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "external_identity_org_user": { + "name": "external_identity_org_user", + "columns": [ + "organization_id", + "user_id" + ], + "isUnique": true + }, + "external_identity_org_sso_remote": { + "name": "external_identity_org_sso_remote", + "columns": [ + "organization_id", + "sso_provider_id", + "remote_id" + ], + "isUnique": true + }, + "external_identity_org_scim_external": { + "name": "external_identity_org_scim_external", + "columns": [ + "organization_id", + "scim_provider_id", + "external_id" + ], + "isUnique": true + }, + "external_identity_org_email": { + "name": "external_identity_org_email", + "columns": [ + "organization_id", + "email" + ], + "isUnique": false + }, + "external_identity_sso_provider": { + "name": "external_identity_sso_provider", + "columns": [ + "sso_provider_id" + ], + "isUnique": false + }, + "external_identity_scim_provider": { + "name": "external_identity_scim_provider", + "columns": [ + "scim_provider_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "external_identity_id": { + "name": "external_identity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oauthAccessToken": { + "name": "oauthAccessToken", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth_access_token_token": { + "name": "oauth_access_token_token", + "columns": [ + "`token`(191)" + ], + "isUnique": false + }, + "oauth_access_token_client_id": { + "name": "oauth_access_token_client_id", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauth_access_token_session_id": { + "name": "oauth_access_token_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "oauth_access_token_user_id": { + "name": "oauth_access_token_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "oauth_access_token_reference_id": { + "name": "oauth_access_token_reference_id", + "columns": [ + "reference_id" + ], + "isUnique": false + }, + "oauth_access_token_refresh_id": { + "name": "oauth_access_token_refresh_id", + "columns": [ + "refresh_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauthAccessToken_id": { + "name": "oauthAccessToken_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oauthClient": { + "name": "oauthClient", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contacts": { + "name": "contacts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_id": { + "name": "software_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_version": { + "name": "software_version", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grant_types": { + "name": "grant_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_types": { + "name": "response_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "oauth_client_client_id": { + "name": "oauth_client_client_id", + "columns": [ + "client_id" + ], + "isUnique": true + }, + "oauth_client_user_id": { + "name": "oauth_client_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "oauth_client_reference_id": { + "name": "oauth_client_reference_id", + "columns": [ + "reference_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauthClient_id": { + "name": "oauthClient_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oauthConsent": { + "name": "oauthConsent", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "oauth_consent_client_id": { + "name": "oauth_consent_client_id", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauth_consent_user_id": { + "name": "oauth_consent_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "oauth_consent_reference_id": { + "name": "oauth_consent_reference_id", + "columns": [ + "reference_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauthConsent_id": { + "name": "oauthConsent_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "oauthRefreshToken": { + "name": "oauthRefreshToken", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "revoked": { + "name": "revoked", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth_refresh_token_client_id": { + "name": "oauth_refresh_token_client_id", + "columns": [ + "client_id" + ], + "isUnique": false + }, + "oauth_refresh_token_session_id": { + "name": "oauth_refresh_token_session_id", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "oauth_refresh_token_user_id": { + "name": "oauth_refresh_token_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "oauth_refresh_token_reference_id": { + "name": "oauth_refresh_token_reference_id", + "columns": [ + "reference_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauthRefreshToken_id": { + "name": "oauthRefreshToken_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "scim_provider": { + "name": "scim_provider", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scim_token": { + "name": "scim_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_mapping_mode": { + "name": "group_mapping_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'metadata_only'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "scim_provider_provider_id": { + "name": "scim_provider_provider_id", + "columns": [ + "provider_id" + ], + "isUnique": true + }, + "scim_provider_organization_id": { + "name": "scim_provider_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scim_provider_id": { + "name": "scim_provider_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "scim_sync_event": { + "name": "scim_sync_event", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "scim_sync_event_org_status": { + "name": "scim_sync_event_org_status", + "columns": [ + "organization_id", + "status" + ], + "isUnique": false + }, + "scim_sync_event_provider_status": { + "name": "scim_sync_event_provider_status", + "columns": [ + "provider_id", + "status" + ], + "isUnique": false + }, + "scim_sync_event_next_retry": { + "name": "scim_sync_event_next_retry", + "columns": [ + "next_retry_at" + ], + "isUnique": false + }, + "scim_sync_event_user": { + "name": "scim_sync_event_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scim_sync_event_id": { + "name": "scim_sync_event_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sso_connection": { + "name": "sso_connection", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'enabled'" + }, + "sign_in_path": { + "name": "sign_in_path", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_tested_at": { + "name": "last_tested_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "sso_connection_organization_id": { + "name": "sso_connection_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": true + }, + "sso_connection_provider_id": { + "name": "sso_connection_provider_id", + "columns": [ + "provider_id" + ], + "isUnique": true + }, + "sso_connection_domain": { + "name": "sso_connection_domain", + "columns": [ + "domain" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_connection_id": { + "name": "sso_connection_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sso_provider": { + "name": "sso_provider", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "sso_provider_provider_id": { + "name": "sso_provider_provider_id", + "columns": [ + "provider_id" + ], + "isUnique": true + }, + "sso_provider_domain": { + "name": "sso_provider_domain", + "columns": [ + "domain" + ], + "isUnique": false + }, + "sso_provider_organization_id": { + "name": "sso_provider_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "sso_provider_user_id": { + "name": "sso_provider_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_provider_id": { + "name": "sso_provider_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "desktop_policy_member": { + "name": "desktop_policy_member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "desktop_policy_id": { + "name": "desktop_policy_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_member_id": { + "name": "org_member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "desktop_policy_member_organization_id": { + "name": "desktop_policy_member_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "desktop_policy_member_org_member_id": { + "name": "desktop_policy_member_org_member_id", + "columns": [ + "org_member_id" + ], + "isUnique": false + }, + "desktop_policy_member_team_id": { + "name": "desktop_policy_member_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "desktop_policy_member_policy_org_member": { + "name": "desktop_policy_member_policy_org_member", + "columns": [ + "desktop_policy_id", + "org_member_id" + ], + "isUnique": true + }, + "desktop_policy_member_policy_team": { + "name": "desktop_policy_member_policy_team", + "columns": [ + "desktop_policy_id", + "team_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "desktop_policy_member_id": { + "name": "desktop_policy_member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "desktop_policy": { + "name": "desktop_policy", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "policy_name": { + "name": "policy_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "priority": { + "name": "priority", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "policy": { + "name": "policy", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_object())" + }, + "created_by_org_member_id": { + "name": "created_by_org_member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "desktop_policy_created_by_member_id": { + "name": "desktop_policy_created_by_member_id", + "columns": [ + "created_by_org_member_id" + ], + "isUnique": false + }, + "desktop_policy_is_enabled": { + "name": "desktop_policy_is_enabled", + "columns": [ + "is_enabled" + ], + "isUnique": false + }, + "desktop_policy_priority": { + "name": "desktop_policy_priority", + "columns": [ + "priority" + ], + "isUnique": false + }, + "desktop_policy_deleted_at": { + "name": "desktop_policy_deleted_at", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "desktop_policy_org_default": { + "name": "desktop_policy_org_default", + "columns": [ + "organization_id", + "is_default" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "desktop_policy_id": { + "name": "desktop_policy_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "organization_diagnostic_credential": { + "name": "organization_diagnostic_credential", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bearer_token": { + "name": "bearer_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "organization_diagnostic_credential_organization_id": { + "name": "organization_diagnostic_credential_organization_id", + "columns": [ + "organization_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "inference_keys": { + "name": "inference_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_hash": { + "name": "key_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_prefix": { + "name": "key_prefix", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','revoked')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "inference_keys_key_hash": { + "name": "inference_keys_key_hash", + "columns": [ + "key_hash" + ], + "isUnique": true + }, + "inference_keys_organization_id": { + "name": "inference_keys_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "inference_keys_org_membership_id": { + "name": "inference_keys_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "inference_keys_status": { + "name": "inference_keys_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inference_keys_id": { + "name": "inference_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "inference_org_limit_policies": { + "name": "inference_org_limit_policies", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_type": { + "name": "window_type", + "type": "enum('five_hour','weekly','monthly')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reset_strategy": { + "name": "reset_strategy", + "type": "enum('anchored','activity_based')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "anchor_at": { + "name": "anchor_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_bucket_id": { + "name": "current_bucket_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "inference_org_limit_policies_org_window_type": { + "name": "inference_org_limit_policies_org_window_type", + "columns": [ + "organization_id", + "window_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inference_org_limit_policies_id": { + "name": "inference_org_limit_policies_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "inference_org_upstream_provider_keys": { + "name": "inference_org_upstream_provider_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openrouter'" + }, + "external_key_hash": { + "name": "external_key_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_workspace_id": { + "name": "external_workspace_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_prefix": { + "name": "key_prefix", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','revoked')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "inference_org_upstream_provider_keys_external_key_hash": { + "name": "inference_org_upstream_provider_keys_external_key_hash", + "columns": [ + "external_key_hash" + ], + "isUnique": false + }, + "inference_org_upstream_provider_keys_org_provider": { + "name": "inference_org_upstream_provider_keys_org_provider", + "columns": [ + "organization_id", + "provider" + ], + "isUnique": true + }, + "inference_org_upstream_provider_keys_status": { + "name": "inference_org_upstream_provider_keys_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inference_org_upstream_provider_keys_id": { + "name": "inference_org_upstream_provider_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "inference_org_usage_buckets": { + "name": "inference_org_usage_buckets", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "policy_id": { + "name": "policy_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_start_at": { + "name": "window_start_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "window_end_at": { + "name": "window_end_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "limit_amount": { + "name": "limit_amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_amount": { + "name": "used_amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "inference_org_usage_buckets_org_window": { + "name": "inference_org_usage_buckets_org_window", + "columns": [ + "organization_id", + "window_start_at", + "window_end_at" + ], + "isUnique": false + }, + "inference_org_usage_buckets_policy_window": { + "name": "inference_org_usage_buckets_policy_window", + "columns": [ + "policy_id", + "window_start_at", + "window_end_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inference_org_usage_buckets_id": { + "name": "inference_org_usage_buckets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "inference_usage_ledger_bucket_charges": { + "name": "inference_usage_ledger_bucket_charges", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ledger_entry_id": { + "name": "ledger_entry_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bucket_id": { + "name": "bucket_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "inference_usage_ledger_bucket_charges_bucket_id": { + "name": "inference_usage_ledger_bucket_charges_bucket_id", + "columns": [ + "bucket_id" + ], + "isUnique": false + }, + "inference_usage_ledger_bucket_charges_entry_bucket": { + "name": "inference_usage_ledger_bucket_charges_entry_bucket", + "columns": [ + "ledger_entry_id", + "bucket_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inference_usage_ledger_bucket_charges_id": { + "name": "inference_usage_ledger_bucket_charges_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "inference_usage_ledger_entries": { + "name": "inference_usage_ledger_entries", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inference_key_id": { + "name": "inference_key_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_job_id": { + "name": "external_job_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_event_id": { + "name": "external_event_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_amount": { + "name": "cost_amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "inference_usage_ledger_entries_organization_id": { + "name": "inference_usage_ledger_entries_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "inference_usage_ledger_entries_org_membership_id": { + "name": "inference_usage_ledger_entries_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "inference_usage_ledger_entries_inference_key_id": { + "name": "inference_usage_ledger_entries_inference_key_id", + "columns": [ + "inference_key_id" + ], + "isUnique": false + }, + "inference_usage_ledger_entries_external_event_id": { + "name": "inference_usage_ledger_entries_external_event_id", + "columns": [ + "external_event_id" + ], + "isUnique": true + }, + "inference_usage_ledger_entries_job_event_type": { + "name": "inference_usage_ledger_entries_job_event_type", + "columns": [ + "external_job_id", + "event_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inference_usage_ledger_entries_id": { + "name": "inference_usage_ledger_entries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "memory_context": { + "name": "memory_context", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_id": { + "name": "memory_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "citation": { + "name": "citation", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "snippet": { + "name": "snippet", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "enum('active_conversation','searched_conversation')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "memory_context_memory_id": { + "name": "memory_context_memory_id", + "columns": [ + "memory_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "memory_context_id": { + "name": "memory_context_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "memory": { + "name": "memory", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('user','org')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3)" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "memory_user_id": { + "name": "memory_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "memory_id": { + "name": "memory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "desktop_connect_grant": { + "name": "desktop_connect_grant", + "columns": { + "code_hash": { + "name": "code_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "install_link_id": { + "name": "install_link_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claims": { + "name": "claims", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "consumed_nonce": { + "name": "consumed_nonce", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "desktop_connect_grant_install_link_id": { + "name": "desktop_connect_grant_install_link_id", + "columns": [ + "install_link_id" + ], + "isUnique": false + }, + "desktop_connect_grant_expires_at": { + "name": "desktop_connect_grant_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "desktop_connect_grant_code_hash": { + "name": "desktop_connect_grant_code_hash", + "columns": [ + "code_hash" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "desktop_handoff_grant": { + "name": "desktop_handoff_grant", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "desktop_handoff_grant_user_id": { + "name": "desktop_handoff_grant_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "desktop_handoff_grant_expires_at": { + "name": "desktop_handoff_grant_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "desktop_handoff_grant_id": { + "name": "desktop_handoff_grant_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "install_link": { + "name": "install_link", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "install_link_token_hash": { + "name": "install_link_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "install_link_organization_id": { + "name": "install_link_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "install_link_created_by_user_id": { + "name": "install_link_created_by_user_id", + "columns": [ + "created_by_user_id" + ], + "isUnique": false + }, + "install_link_revoked_at": { + "name": "install_link_revoked_at", + "columns": [ + "revoked_at" + ], + "isUnique": false + }, + "install_link_expires_at": { + "name": "install_link_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "install_link_id": { + "name": "install_link_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_member_id": { + "name": "org_member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invite_token": { + "name": "invite_token", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "invitation_organization_id": { + "name": "invitation_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email": { + "name": "invitation_email", + "columns": [ + "email" + ], + "isUnique": false + }, + "invitation_status": { + "name": "invitation_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "invitation_team_id": { + "name": "invitation_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "invitation_inviter_id": { + "name": "invitation_inviter_id", + "columns": [ + "inviter_id" + ], + "isUnique": false + }, + "invitation_org_member_id": { + "name": "invitation_org_member_id", + "columns": [ + "org_member_id" + ], + "isUnique": false + }, + "invitation_invite_token": { + "name": "invitation_invite_token", + "columns": [ + "invite_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invitation_id": { + "name": "invitation_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invite_id": { + "name": "invite_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invited_by_org_member": { + "name": "invited_by_org_member", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(now())" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "removed_by_org_member": { + "name": "removed_by_org_member", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "member_user_id": { + "name": "member_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "member_invite_id": { + "name": "member_invite_id", + "columns": [ + "invite_id" + ], + "isUnique": false + }, + "member_invited_by_org_member": { + "name": "member_invited_by_org_member", + "columns": [ + "invited_by_org_member" + ], + "isUnique": false + }, + "member_removed_at": { + "name": "member_removed_at", + "columns": [ + "removed_at" + ], + "isUnique": false + }, + "member_removed_by_org_member": { + "name": "member_removed_by_org_member", + "columns": [ + "removed_by_org_member" + ], + "isUnique": false + }, + "member_organization_user": { + "name": "member_organization_user", + "columns": [ + "organization_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "member_id": { + "name": "member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "organization_brand_asset": { + "name": "organization_brand_asset", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "extension": { + "name": "extension", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "mediumblob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "organization_brand_asset_version": { + "name": "organization_brand_asset_version", + "columns": [ + "organization_id", + "kind", + "version", + "extension" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "organization_brand_asset_id": { + "name": "organization_brand_asset_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "organization_role": { + "name": "organization_role", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "organization_role_name": { + "name": "organization_role_name", + "columns": [ + "organization_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "organization_role_id": { + "name": "organization_role_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_email_domains": { + "name": "allowed_email_domains", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "desktop_app_restrictions": { + "name": "desktop_app_restrictions", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_object())" + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "organization_slug": { + "name": "organization_slug", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organization_created_at_id": { + "name": "organization_created_at_id", + "columns": [ + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "organization_id": { + "name": "organization_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace_bootstrap": { + "name": "workspace_bootstrap", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "setup_member_id": { + "name": "setup_member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_public_key": { + "name": "device_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "device_key_fingerprint": { + "name": "device_key_fingerprint", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisional'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "workspace_bootstrap_organization_id": { + "name": "workspace_bootstrap_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "workspace_bootstrap_status": { + "name": "workspace_bootstrap_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "workspace_bootstrap_expires_at": { + "name": "workspace_bootstrap_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_bootstrap_id": { + "name": "workspace_bootstrap_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace_claim": { + "name": "workspace_claim", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bootstrap_id": { + "name": "bootstrap_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claimed_by_user_id": { + "name": "claimed_by_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "workspace_claim_token_hash": { + "name": "workspace_claim_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "workspace_claim_bootstrap_id": { + "name": "workspace_claim_bootstrap_id", + "columns": [ + "bootstrap_id" + ], + "isUnique": false + }, + "workspace_claim_organization_id": { + "name": "workspace_claim_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "workspace_claim_status": { + "name": "workspace_claim_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "workspace_claim_expires_at": { + "name": "workspace_claim_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_claim_id": { + "name": "workspace_claim_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "scim_group_member": { + "name": "scim_group_member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "varchar(191)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_member_id": { + "name": "team_member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "scim_group_member_group_remote_user": { + "name": "scim_group_member_group_remote_user", + "columns": [ + "group_id", + "remote_user_id" + ], + "isUnique": true + }, + "scim_group_member_user_id": { + "name": "scim_group_member_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "scim_group_member_org_membership_id": { + "name": "scim_group_member_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "scim_group_member_team_member_id": { + "name": "scim_group_member_team_member_id", + "columns": [ + "team_member_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scim_group_member_id": { + "name": "scim_group_member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "scim_group": { + "name": "scim_group", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(191)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "scim_group_provider_external_id": { + "name": "scim_group_provider_external_id", + "columns": [ + "provider_id", + "external_id" + ], + "isUnique": true + }, + "scim_group_team_id": { + "name": "scim_group_team_id", + "columns": [ + "team_id" + ], + "isUnique": true + }, + "scim_group_organization_id": { + "name": "scim_group_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scim_group_id": { + "name": "scim_group_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "scim_user_tombstone": { + "name": "scim_user_tombstone", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deprovisioned_user_id": { + "name": "deprovisioned_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(191)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(191)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deprovisioned_at": { + "name": "deprovisioned_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "scim_user_tombstone_org_user": { + "name": "scim_user_tombstone_org_user", + "columns": [ + "organization_id", + "deprovisioned_user_id" + ], + "isUnique": true + }, + "scim_user_tombstone_org_external_id": { + "name": "scim_user_tombstone_org_external_id", + "columns": [ + "organization_id", + "external_id" + ], + "isUnique": false + }, + "scim_user_tombstone_org_email": { + "name": "scim_user_tombstone_org_email", + "columns": [ + "organization_id", + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scim_user_tombstone_id": { + "name": "scim_user_tombstone_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connected_account": { + "name": "connected_account", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_account_id": { + "name": "external_account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_code_verifier": { + "name": "pending_code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_health": { + "name": "credential_health", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "connected_account_organization_id": { + "name": "connected_account_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "connected_account_member_provider": { + "name": "connected_account_member_provider", + "columns": [ + "org_membership_id", + "provider_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connected_account_id": { + "name": "connected_account_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "external_mcp_connection_access_grant": { + "name": "external_mcp_connection_access_grant", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_mcp_connection_id": { + "name": "external_mcp_connection_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_mcp_requirement_binding_id": { + "name": "plugin_mcp_requirement_binding_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_key": { + "name": "source_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "org_wide": { + "name": "org_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "emc_access_grant_organization_id": { + "name": "emc_access_grant_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "emc_access_grant_connection_id": { + "name": "emc_access_grant_connection_id", + "columns": [ + "external_mcp_connection_id" + ], + "isUnique": false + }, + "emc_access_grant_plugin_mcp_binding_id": { + "name": "emc_access_grant_plugin_mcp_binding_id", + "columns": [ + "plugin_mcp_requirement_binding_id" + ], + "isUnique": false + }, + "emc_access_grant_org_membership_id": { + "name": "emc_access_grant_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "emc_access_grant_team_id": { + "name": "emc_access_grant_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "emc_access_grant_connection_member": { + "name": "emc_access_grant_connection_member", + "columns": [ + "external_mcp_connection_id", + "org_membership_id", + "source_key" + ], + "isUnique": true + }, + "emc_access_grant_connection_team": { + "name": "emc_access_grant_connection_team", + "columns": [ + "external_mcp_connection_id", + "team_id", + "source_key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "external_mcp_connection_access_grant_id": { + "name": "external_mcp_connection_access_grant_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "external_mcp_connection": { + "name": "external_mcp_connection", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "enum('oauth','apikey','none')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oauth_configuration": { + "name": "oauth_configuration", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_mode": { + "name": "credential_mode", + "type": "enum('shared','per_member')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'shared'" + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pending_code_verifier": { + "name": "pending_code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_health": { + "name": "credential_health", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_issuer_review_required_at": { + "name": "oauth_issuer_review_required_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "external_mcp_connection_organization_id": { + "name": "external_mcp_connection_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "external_mcp_connection_id": { + "name": "external_mcp_connection_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "org_oauth_client": { + "name": "org_oauth_client", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extra": { + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "org_oauth_client_org_provider": { + "name": "org_oauth_client_org_provider", + "columns": [ + "organization_id", + "provider_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_oauth_client_id": { + "name": "org_oauth_client_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_mcp_requirement_binding": { + "name": "plugin_mcp_requirement_binding", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_object_id": { + "name": "config_object_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_mcp_connection_id": { + "name": "external_mcp_connection_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "required_auth_type": { + "name": "required_auth_type", + "type": "enum('oauth','apikey','none')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_owned_by_plugin": { + "name": "connection_owned_by_plugin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "plugin_mcp_req_binding_plugin_id": { + "name": "plugin_mcp_req_binding_plugin_id", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "plugin_mcp_req_binding_config_object_id": { + "name": "plugin_mcp_req_binding_config_object_id", + "columns": [ + "config_object_id" + ], + "isUnique": false + }, + "plugin_mcp_req_binding_connection_id": { + "name": "plugin_mcp_req_binding_connection_id", + "columns": [ + "external_mcp_connection_id" + ], + "isUnique": false + }, + "plugin_mcp_req_binding_org_plugin_object_server": { + "name": "plugin_mcp_req_binding_org_plugin_object_server", + "columns": [ + "organization_id", + "plugin_id", + "config_object_id", + "server_name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_mcp_requirement_binding_id": { + "name": "plugin_mcp_requirement_binding_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llm_provider_access": { + "name": "llm_provider_access", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "llm_provider_id": { + "name": "llm_provider_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "llm_provider_access_org_membership_id": { + "name": "llm_provider_access_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "llm_provider_access_team_id": { + "name": "llm_provider_access_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "llm_provider_access_provider_org_membership": { + "name": "llm_provider_access_provider_org_membership", + "columns": [ + "llm_provider_id", + "org_membership_id" + ], + "isUnique": true + }, + "llm_provider_access_provider_team": { + "name": "llm_provider_access_provider_team", + "columns": [ + "llm_provider_id", + "team_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llm_provider_access_id": { + "name": "llm_provider_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llm_provider_model": { + "name": "llm_provider_model", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "llm_provider_id": { + "name": "llm_provider_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_config": { + "name": "model_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "llm_provider_model_model_id": { + "name": "llm_provider_model_model_id", + "columns": [ + "model_id" + ], + "isUnique": false + }, + "llm_provider_model_provider_model": { + "name": "llm_provider_model_provider_model", + "columns": [ + "llm_provider_id", + "model_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llm_provider_model_id": { + "name": "llm_provider_model_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llm_provider": { + "name": "llm_provider", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "enum('models_dev','custom','openwork')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "llm_provider_organization_id": { + "name": "llm_provider_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "llm_provider_created_by_org_membership_id": { + "name": "llm_provider_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "llm_provider_source": { + "name": "llm_provider_source", + "columns": [ + "source" + ], + "isUnique": false + }, + "llm_provider_provider_id": { + "name": "llm_provider_provider_id", + "columns": [ + "provider_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llm_provider_id": { + "name": "llm_provider_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "config_object_access_grant": { + "name": "config_object_access_grant", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_object_id": { + "name": "config_object_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "org_wide": { + "name": "org_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "role": { + "name": "role", + "type": "enum('viewer','editor','manager')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "config_object_access_grant_organization_id": { + "name": "config_object_access_grant_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "config_object_access_grant_org_membership_id": { + "name": "config_object_access_grant_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "config_object_access_grant_team_id": { + "name": "config_object_access_grant_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "config_object_access_grant_org_wide": { + "name": "config_object_access_grant_org_wide", + "columns": [ + "org_wide" + ], + "isUnique": false + }, + "config_object_access_grant_object_org_membership": { + "name": "config_object_access_grant_object_org_membership", + "columns": [ + "config_object_id", + "org_membership_id" + ], + "isUnique": true + }, + "config_object_access_grant_object_team": { + "name": "config_object_access_grant_object_team", + "columns": [ + "config_object_id", + "team_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "config_object_access_grant_id": { + "name": "config_object_access_grant_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "config_object": { + "name": "config_object", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_type": { + "name": "object_type", + "type": "enum('skill','agent','command','tool','mcp','hook','context','custom')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_mode": { + "name": "source_mode", + "type": "enum('cloud','import','connector')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "search_text": { + "name": "search_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_file_name": { + "name": "current_file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_file_extension": { + "name": "current_file_extension", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_relative_path": { + "name": "current_relative_path", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','inactive','deleted','archived','ingestion_error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "config_object_organization_id": { + "name": "config_object_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "config_object_type": { + "name": "config_object_type", + "columns": [ + "object_type" + ], + "isUnique": false + }, + "config_object_source_mode": { + "name": "config_object_source_mode", + "columns": [ + "source_mode" + ], + "isUnique": false + }, + "config_object_status": { + "name": "config_object_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "config_object_created_by_org_membership_id": { + "name": "config_object_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "config_object_connector_instance_id": { + "name": "config_object_connector_instance_id", + "columns": [ + "connector_instance_id" + ], + "isUnique": false + }, + "config_object_current_relative_path": { + "name": "config_object_current_relative_path", + "columns": [ + "current_relative_path" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "config_object_id": { + "name": "config_object_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "config_object_version": { + "name": "config_object_version", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_object_id": { + "name": "config_object_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_payload_json": { + "name": "normalized_payload_json", + "type": "mediumtext", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_source_text": { + "name": "raw_source_text", + "type": "mediumtext", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_via": { + "name": "created_via", + "type": "enum('cloud','import','connector','system')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connector_sync_event_id": { + "name": "connector_sync_event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_revision_ref": { + "name": "source_revision_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_deleted_version": { + "name": "is_deleted_version", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "config_object_version_organization_id": { + "name": "config_object_version_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "config_object_version_created_by_org_membership_id": { + "name": "config_object_version_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "config_object_version_connector_sync_event_id": { + "name": "config_object_version_connector_sync_event_id", + "columns": [ + "connector_sync_event_id" + ], + "isUnique": false + }, + "config_object_version_source_revision_ref": { + "name": "config_object_version_source_revision_ref", + "columns": [ + "source_revision_ref" + ], + "isUnique": false + }, + "config_object_version_lookup_latest": { + "name": "config_object_version_lookup_latest", + "columns": [ + "config_object_id", + "created_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "config_object_version_id": { + "name": "config_object_version_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connector_account": { + "name": "connector_account", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_type": { + "name": "connector_type", + "type": "enum('github')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_id": { + "name": "remote_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_account_ref": { + "name": "external_account_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','inactive','disconnected','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "connector_account_created_by_org_membership_id": { + "name": "connector_account_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "connector_account_connector_type": { + "name": "connector_account_connector_type", + "columns": [ + "connector_type" + ], + "isUnique": false + }, + "idx_connector_account_on_remote_id": { + "name": "idx_connector_account_on_remote_id", + "columns": [ + "remote_id" + ], + "isUnique": false + }, + "connector_account_status": { + "name": "connector_account_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "connector_account_org_type_remote_id": { + "name": "connector_account_org_type_remote_id", + "columns": [ + "organization_id", + "connector_type", + "remote_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connector_account_id": { + "name": "connector_account_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connector_instance_access_grant": { + "name": "connector_instance_access_grant", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "org_wide": { + "name": "org_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "role": { + "name": "role", + "type": "enum('viewer','editor','manager')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "connector_instance_access_grant_organization_id": { + "name": "connector_instance_access_grant_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "connector_instance_access_grant_org_membership_id": { + "name": "connector_instance_access_grant_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "connector_instance_access_grant_team_id": { + "name": "connector_instance_access_grant_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "connector_instance_access_grant_org_wide": { + "name": "connector_instance_access_grant_org_wide", + "columns": [ + "org_wide" + ], + "isUnique": false + }, + "connector_instance_access_grant_instance_org_membership": { + "name": "connector_instance_access_grant_instance_org_membership", + "columns": [ + "connector_instance_id", + "org_membership_id" + ], + "isUnique": true + }, + "connector_instance_access_grant_instance_team": { + "name": "connector_instance_access_grant_instance_team", + "columns": [ + "connector_instance_id", + "team_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connector_instance_access_grant_id": { + "name": "connector_instance_access_grant_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connector_instance": { + "name": "connector_instance", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_account_id": { + "name": "connector_account_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_type": { + "name": "connector_type", + "type": "enum('github')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_id": { + "name": "remote_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','disabled','archived','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "instance_config_json": { + "name": "instance_config_json", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync_status": { + "name": "last_sync_status", + "type": "enum('pending','queued','running','completed','failed','partial','ignored')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync_cursor": { + "name": "last_sync_cursor", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "connector_instance_connector_account_id": { + "name": "connector_instance_connector_account_id", + "columns": [ + "connector_account_id" + ], + "isUnique": false + }, + "connector_instance_created_by_org_membership_id": { + "name": "connector_instance_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "connector_instance_connector_type": { + "name": "connector_instance_connector_type", + "columns": [ + "connector_type" + ], + "isUnique": false + }, + "connector_instance_status": { + "name": "connector_instance_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "connector_instance_org_name": { + "name": "connector_instance_org_name", + "columns": [ + "organization_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connector_instance_id": { + "name": "connector_instance_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connector_mapping": { + "name": "connector_mapping", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_target_id": { + "name": "connector_target_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_type": { + "name": "connector_type", + "type": "enum('github')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_id": { + "name": "remote_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mapping_kind": { + "name": "mapping_kind", + "type": "enum('path','api','custom')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "selector": { + "name": "selector", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_type": { + "name": "object_type", + "type": "enum('skill','agent','command','tool','mcp','hook','context','custom')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_add_to_plugin": { + "name": "auto_add_to_plugin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "mapping_config_json": { + "name": "mapping_config_json", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "connector_mapping_organization_id": { + "name": "connector_mapping_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "connector_mapping_connector_instance_id": { + "name": "connector_mapping_connector_instance_id", + "columns": [ + "connector_instance_id" + ], + "isUnique": false + }, + "connector_mapping_object_type": { + "name": "connector_mapping_object_type", + "columns": [ + "object_type" + ], + "isUnique": false + }, + "connector_mapping_plugin_id": { + "name": "connector_mapping_plugin_id", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "connector_mapping_target_selector_object_type": { + "name": "connector_mapping_target_selector_object_type", + "columns": [ + "connector_target_id", + "selector", + "object_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connector_mapping_id": { + "name": "connector_mapping_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connector_source_binding": { + "name": "connector_source_binding", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_object_id": { + "name": "config_object_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_target_id": { + "name": "connector_target_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_mapping_id": { + "name": "connector_mapping_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_type": { + "name": "connector_type", + "type": "enum('github')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_id": { + "name": "remote_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_locator": { + "name": "external_locator", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_stable_ref": { + "name": "external_stable_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_seen_source_revision_ref": { + "name": "last_seen_source_revision_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','inactive','deleted','archived','ingestion_error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "connector_source_binding_organization_id": { + "name": "connector_source_binding_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "connector_source_binding_connector_instance_id": { + "name": "connector_source_binding_connector_instance_id", + "columns": [ + "connector_instance_id" + ], + "isUnique": false + }, + "connector_source_binding_connector_target_id": { + "name": "connector_source_binding_connector_target_id", + "columns": [ + "connector_target_id" + ], + "isUnique": false + }, + "connector_source_binding_connector_mapping_id": { + "name": "connector_source_binding_connector_mapping_id", + "columns": [ + "connector_mapping_id" + ], + "isUnique": false + }, + "connector_source_binding_external_locator": { + "name": "connector_source_binding_external_locator", + "columns": [ + "external_locator" + ], + "isUnique": false + }, + "connector_source_binding_config_object": { + "name": "connector_source_binding_config_object", + "columns": [ + "config_object_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connector_source_binding_id": { + "name": "connector_source_binding_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connector_source_tombstone": { + "name": "connector_source_tombstone", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_target_id": { + "name": "connector_target_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_mapping_id": { + "name": "connector_mapping_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_type": { + "name": "connector_type", + "type": "enum('github')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_id": { + "name": "remote_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_locator": { + "name": "external_locator", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "former_config_object_id": { + "name": "former_config_object_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_in_sync_event_id": { + "name": "deleted_in_sync_event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_source_revision_ref": { + "name": "deleted_source_revision_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "connector_source_tombstone_organization_id": { + "name": "connector_source_tombstone_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "connector_source_tombstone_connector_instance_id": { + "name": "connector_source_tombstone_connector_instance_id", + "columns": [ + "connector_instance_id" + ], + "isUnique": false + }, + "connector_source_tombstone_connector_target_id": { + "name": "connector_source_tombstone_connector_target_id", + "columns": [ + "connector_target_id" + ], + "isUnique": false + }, + "connector_source_tombstone_connector_mapping_id": { + "name": "connector_source_tombstone_connector_mapping_id", + "columns": [ + "connector_mapping_id" + ], + "isUnique": false + }, + "connector_source_tombstone_external_locator": { + "name": "connector_source_tombstone_external_locator", + "columns": [ + "external_locator" + ], + "isUnique": false + }, + "connector_source_tombstone_former_config_object_id": { + "name": "connector_source_tombstone_former_config_object_id", + "columns": [ + "former_config_object_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connector_source_tombstone_id": { + "name": "connector_source_tombstone_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connector_sync_event": { + "name": "connector_sync_event", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_target_id": { + "name": "connector_target_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connector_type": { + "name": "connector_type", + "type": "enum('github')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_id": { + "name": "remote_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "enum('push','installation','installation_repositories','repository','manual_resync')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_event_ref": { + "name": "external_event_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_revision_ref": { + "name": "source_revision_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','queued','running','completed','failed','partial','ignored')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "summary_json": { + "name": "summary_json", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "connector_sync_event_organization_id": { + "name": "connector_sync_event_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "connector_sync_event_connector_instance_id": { + "name": "connector_sync_event_connector_instance_id", + "columns": [ + "connector_instance_id" + ], + "isUnique": false + }, + "connector_sync_event_connector_target_id": { + "name": "connector_sync_event_connector_target_id", + "columns": [ + "connector_target_id" + ], + "isUnique": false + }, + "connector_sync_event_event_type": { + "name": "connector_sync_event_event_type", + "columns": [ + "event_type" + ], + "isUnique": false + }, + "connector_sync_event_status": { + "name": "connector_sync_event_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "connector_sync_event_source_revision_ref": { + "name": "connector_sync_event_source_revision_ref", + "columns": [ + "source_revision_ref" + ], + "isUnique": false + }, + "connector_sync_event_external_event_ref": { + "name": "connector_sync_event_external_event_ref", + "columns": [ + "external_event_ref" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connector_sync_event_id": { + "name": "connector_sync_event_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "connector_target": { + "name": "connector_target", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connector_type": { + "name": "connector_type", + "type": "enum('github')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remote_id": { + "name": "remote_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_kind": { + "name": "target_kind", + "type": "enum('repository_branch')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_target_ref": { + "name": "external_target_ref", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_config_json": { + "name": "target_config_json", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "connector_target_organization_id": { + "name": "connector_target_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "connector_target_connector_type": { + "name": "connector_target_connector_type", + "columns": [ + "connector_type" + ], + "isUnique": false + }, + "connector_target_target_kind": { + "name": "connector_target_target_kind", + "columns": [ + "target_kind" + ], + "isUnique": false + }, + "connector_target_instance_remote_id": { + "name": "connector_target_instance_remote_id", + "columns": [ + "connector_instance_id", + "remote_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connector_target_id": { + "name": "connector_target_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "marketplace_access_grant": { + "name": "marketplace_access_grant", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "marketplace_id": { + "name": "marketplace_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "org_wide": { + "name": "org_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "role": { + "name": "role", + "type": "enum('viewer','editor','manager')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "marketplace_access_grant_organization_id": { + "name": "marketplace_access_grant_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "marketplace_access_grant_org_membership_id": { + "name": "marketplace_access_grant_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "marketplace_access_grant_team_id": { + "name": "marketplace_access_grant_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "marketplace_access_grant_org_wide": { + "name": "marketplace_access_grant_org_wide", + "columns": [ + "org_wide" + ], + "isUnique": false + }, + "marketplace_access_grant_marketplace_org_membership": { + "name": "marketplace_access_grant_marketplace_org_membership", + "columns": [ + "marketplace_id", + "org_membership_id" + ], + "isUnique": true + }, + "marketplace_access_grant_marketplace_team": { + "name": "marketplace_access_grant_marketplace_team", + "columns": [ + "marketplace_id", + "team_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "marketplace_access_grant_id": { + "name": "marketplace_access_grant_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "marketplace_plugin": { + "name": "marketplace_plugin", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "marketplace_id": { + "name": "marketplace_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "membership_source": { + "name": "membership_source", + "type": "enum('manual','connector','api','system')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "marketplace_plugin_organization_id": { + "name": "marketplace_plugin_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "marketplace_plugin_plugin_id": { + "name": "marketplace_plugin_plugin_id", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "marketplace_plugin_marketplace_plugin": { + "name": "marketplace_plugin_marketplace_plugin", + "columns": [ + "marketplace_id", + "plugin_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "marketplace_plugin_id": { + "name": "marketplace_plugin_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "marketplace": { + "name": "marketplace", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','inactive','deleted','archived')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "marketplace_organization_id": { + "name": "marketplace_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "marketplace_created_by_org_membership_id": { + "name": "marketplace_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "marketplace_status": { + "name": "marketplace_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "marketplace_name": { + "name": "marketplace_name", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "marketplace_id": { + "name": "marketplace_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_access_grant": { + "name": "plugin_access_grant", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "org_wide": { + "name": "org_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "role": { + "name": "role", + "type": "enum('viewer','editor','manager')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "plugin_access_grant_organization_id": { + "name": "plugin_access_grant_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "plugin_access_grant_org_membership_id": { + "name": "plugin_access_grant_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "plugin_access_grant_team_id": { + "name": "plugin_access_grant_team_id", + "columns": [ + "team_id" + ], + "isUnique": false + }, + "plugin_access_grant_org_wide": { + "name": "plugin_access_grant_org_wide", + "columns": [ + "org_wide" + ], + "isUnique": false + }, + "plugin_access_grant_plugin_org_membership": { + "name": "plugin_access_grant_plugin_org_membership", + "columns": [ + "plugin_id", + "org_membership_id" + ], + "isUnique": true + }, + "plugin_access_grant_plugin_team": { + "name": "plugin_access_grant_plugin_team", + "columns": [ + "plugin_id", + "team_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_access_grant_id": { + "name": "plugin_access_grant_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_config_object": { + "name": "plugin_config_object", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_object_id": { + "name": "config_object_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "membership_source": { + "name": "membership_source", + "type": "enum('manual','connector','api','system')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "connector_mapping_id": { + "name": "connector_mapping_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "plugin_config_object_organization_id": { + "name": "plugin_config_object_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "plugin_config_object_config_object_id": { + "name": "plugin_config_object_config_object_id", + "columns": [ + "config_object_id" + ], + "isUnique": false + }, + "plugin_config_object_connector_mapping_id": { + "name": "plugin_config_object_connector_mapping_id", + "columns": [ + "connector_mapping_id" + ], + "isUnique": false + }, + "plugin_config_object_plugin_config_object": { + "name": "plugin_config_object_plugin_config_object", + "columns": [ + "plugin_id", + "config_object_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_config_object_id": { + "name": "plugin_config_object_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin": { + "name": "plugin", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','inactive','deleted','archived')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "plugin_organization_id": { + "name": "plugin_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "plugin_created_by_org_membership_id": { + "name": "plugin_created_by_org_membership_id", + "columns": [ + "created_by_org_membership_id" + ], + "isUnique": false + }, + "plugin_status": { + "name": "plugin_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "plugin_name": { + "name": "plugin_name", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_id": { + "name": "plugin_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "org_subscriptions": { + "name": "org_subscriptions", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('inference','seat')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('incomplete','incomplete_expired','trialing','active','past_due','canceled','unpaid','paused','expired')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'incomplete'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stripe_subscription_item_id": { + "name": "stripe_subscription_item_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_event_id": { + "name": "last_event_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "org_subscriptions_customer_id": { + "name": "org_subscriptions_customer_id", + "columns": [ + "stripe_customer_id" + ], + "isUnique": false + }, + "org_subscriptions_subscription_id": { + "name": "org_subscriptions_subscription_id", + "columns": [ + "stripe_subscription_id" + ], + "isUnique": true + }, + "org_subscriptions_org_type": { + "name": "org_subscriptions_org_type", + "columns": [ + "organization_id", + "type" + ], + "isUnique": true + }, + "org_subscriptions_status": { + "name": "org_subscriptions_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_subscriptions_id": { + "name": "org_subscriptions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "team_member": { + "name": "team_member", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_membership_id": { + "name": "org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "team_member_org_membership_id": { + "name": "team_member_org_membership_id", + "columns": [ + "org_membership_id" + ], + "isUnique": false + }, + "team_member_team_org_membership": { + "name": "team_member_team_org_membership", + "columns": [ + "team_id", + "org_membership_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "team_member_id": { + "name": "team_member_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "team": { + "name": "team", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "team_organization_name": { + "name": "team_organization_name", + "columns": [ + "organization_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "team_id": { + "name": "team_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "telegram_chat_binding": { + "name": "telegram_chat_binding", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "telegram_user_id": { + "name": "telegram_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "telegram_username": { + "name": "telegram_username", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telegram_first_name": { + "name": "telegram_first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_workspace_id": { + "name": "worker_workspace_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worker_session_id": { + "name": "worker_session_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paired_at": { + "name": "paired_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "telegram_chat_binding_connection_id": { + "name": "telegram_chat_binding_connection_id", + "columns": [ + "connection_id" + ], + "isUnique": true + }, + "telegram_chat_binding_connection_chat": { + "name": "telegram_chat_binding_connection_chat", + "columns": [ + "connection_id", + "telegram_chat_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "telegram_chat_binding_id": { + "name": "telegram_chat_binding_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "telegram_connection": { + "name": "telegram_connection", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_org_membership_id": { + "name": "created_by_org_membership_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bot_token": { + "name": "bot_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bot_id": { + "name": "bot_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bot_username": { + "name": "bot_username", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bot_display_name": { + "name": "bot_display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('active','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "webhook_registered": { + "name": "webhook_registered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "dispatch_token": { + "name": "dispatch_token", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dispatch_started_at": { + "name": "dispatch_started_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_webhook_at": { + "name": "last_webhook_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "telegram_connection_organization_id": { + "name": "telegram_connection_organization_id", + "columns": [ + "organization_id" + ], + "isUnique": true + }, + "telegram_connection_bot_id": { + "name": "telegram_connection_bot_id", + "columns": [ + "bot_id" + ], + "isUnique": true + }, + "telegram_connection_worker_id": { + "name": "telegram_connection_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "telegram_connection_id": { + "name": "telegram_connection_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "telegram_pairing": { + "name": "telegram_pairing", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "telegram_pairing_token_hash": { + "name": "telegram_pairing_token_hash", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "telegram_pairing_connection_id": { + "name": "telegram_pairing_connection_id", + "columns": [ + "connection_id" + ], + "isUnique": false + }, + "telegram_pairing_expires_at": { + "name": "telegram_pairing_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "telegram_pairing_id": { + "name": "telegram_pairing_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "telegram_update": { + "name": "telegram_update", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "update_id": { + "name": "update_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('accepted','processing','completed','ignored','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'accepted'" + }, + "attempts": { + "name": "attempts", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processing_token": { + "name": "processing_token", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "telegram_update_connection_update": { + "name": "telegram_update_connection_update", + "columns": [ + "connection_id", + "update_id" + ], + "isUnique": true + }, + "telegram_update_dispatch": { + "name": "telegram_update_dispatch", + "columns": [ + "status", + "processing_started_at", + "received_at" + ], + "isUnique": false + }, + "telegram_update_received_at": { + "name": "telegram_update_received_at", + "columns": [ + "received_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "telegram_update_id": { + "name": "telegram_update_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_event": { + "name": "audit_event", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "audit_event_org_id": { + "name": "audit_event_org_id", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "audit_event_worker_id": { + "name": "audit_event_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "audit_event_id": { + "name": "audit_event_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "daytona_sandbox": { + "name": "daytona_sandbox", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_volume_id": { + "name": "workspace_volume_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data_volume_id": { + "name": "data_volume_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signed_preview_url": { + "name": "signed_preview_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signed_preview_url_expires_at": { + "name": "signed_preview_url_expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "daytona_sandbox_worker_id": { + "name": "daytona_sandbox_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": true + }, + "daytona_sandbox_sandbox_id": { + "name": "daytona_sandbox_sandbox_id", + "columns": [ + "sandbox_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "daytona_sandbox_id": { + "name": "daytona_sandbox_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_bundle": { + "name": "worker_bundle", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_url": { + "name": "storage_url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "worker_bundle_worker_id": { + "name": "worker_bundle_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_bundle_id": { + "name": "worker_bundle_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_instance": { + "name": "worker_instance", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('provisioning','healthy','failed','stopped')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "worker_instance_worker_id": { + "name": "worker_instance_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_instance_id": { + "name": "worker_instance_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker": { + "name": "worker", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "destination": { + "name": "destination", + "type": "enum('local','cloud')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('provisioning','healthy','failed','stopped')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_version": { + "name": "image_version", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_path": { + "name": "workspace_path", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sandbox_backend": { + "name": "sandbox_backend", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "worker_org_id": { + "name": "worker_org_id", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "worker_created_by_user_id": { + "name": "worker_created_by_user_id", + "columns": [ + "created_by_user_id" + ], + "isUnique": false + }, + "worker_status": { + "name": "worker_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "worker_last_heartbeat_at": { + "name": "worker_last_heartbeat_at", + "columns": [ + "last_heartbeat_at" + ], + "isUnique": false + }, + "worker_last_active_at": { + "name": "worker_last_active_at", + "columns": [ + "last_active_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_id": { + "name": "worker_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "worker_token": { + "name": "worker_token", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worker_id": { + "name": "worker_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "enum('client','host','activity')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "worker_token_worker_id": { + "name": "worker_token_worker_id", + "columns": [ + "worker_id" + ], + "isUnique": false + }, + "worker_token_token": { + "name": "worker_token_token", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "worker_token_id": { + "name": "worker_token_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "admin_allowlist": { + "name": "admin_allowlist", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": { + "admin_allowlist_email": { + "name": "admin_allowlist_email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "admin_allowlist_id": { + "name": "admin_allowlist_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rate_limit": { + "name": "rate_limit", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rate_limit_key": { + "name": "rate_limit_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limit_id": { + "name": "rate_limit_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "telemetry_event": { + "name": "telemetry_event", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "member_id": { + "name": "member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_timestamp": { + "name": "event_timestamp", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "telemetry_event_org_id_type_ts": { + "name": "telemetry_event_org_id_type_ts", + "columns": [ + "org_id", + "event_type", + "event_timestamp" + ], + "isUnique": false + }, + "telemetry_event_org_id_member_id": { + "name": "telemetry_event_org_id_member_id", + "columns": [ + "org_id", + "member_id" + ], + "isUnique": false + }, + "telemetry_event_member_ts": { + "name": "telemetry_event_member_ts", + "columns": [ + "member_id", + "event_timestamp" + ], + "isUnique": false + }, + "telemetry_event_org_session_ts": { + "name": "telemetry_event_org_session_ts", + "columns": [ + "org_id", + "session_id", + "event_timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "telemetry_event_id": { + "name": "telemetry_event_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "telemetry_session_dimension": { + "name": "telemetry_session_dimension", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dimension_type": { + "name": "dimension_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dimension_value": { + "name": "dimension_value", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dimension_label": { + "name": "dimension_label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "telemetry_session_dimension_org_source_session_type": { + "name": "telemetry_session_dimension_org_source_session_type", + "columns": [ + "org_id", + "source", + "session_id", + "dimension_type" + ], + "isUnique": true + }, + "telemetry_session_dimension_filter": { + "name": "telemetry_session_dimension_filter", + "columns": [ + "org_id", + "dimension_type", + "dimension_value", + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "telemetry_session_dimension_id": { + "name": "telemetry_session_dimension_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_artifact_preference": { + "name": "ui_artifact_preference", + "columns": { + "member_id": { + "name": "member_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enabled_artifact_ids": { + "name": "enabled_artifact_ids", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ui_artifact_preference_member_id": { + "name": "ui_artifact_preference_member_id", + "columns": [ + "member_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": { + "oauth_access_token_token": { + "columns": { + "`token`(191)": { + "isExpression": true + } + } + } + } + } +} \ No newline at end of file diff --git a/ee/packages/den-db/drizzle/meta/_journal.json b/ee/packages/den-db/drizzle/meta/_journal.json index 8a080fe842..eb6264c8ed 100644 --- a/ee/packages/den-db/drizzle/meta/_journal.json +++ b/ee/packages/den-db/drizzle/meta/_journal.json @@ -344,6 +344,13 @@ "when": 1784998956602, "tag": "0049_third_caretaker", "breakpoints": true + }, + { + "idx": 50, + "version": "5", + "when": 1785240356970, + "tag": "0050_oval_longshot", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/ee/packages/den-db/src/schema/index.ts b/ee/packages/den-db/src/schema/index.ts index 6e8fd671dc..d54a2a1e4b 100644 --- a/ee/packages/den-db/src/schema/index.ts +++ b/ee/packages/den-db/src/schema/index.ts @@ -14,3 +14,4 @@ export * from "./telegram" export * from "./workers" export * from "./system" export * from "./telemetry" +export * from "./ui-artifacts" diff --git a/ee/packages/den-db/src/schema/ui-artifacts.ts b/ee/packages/den-db/src/schema/ui-artifacts.ts new file mode 100644 index 0000000000..710e3ffd67 --- /dev/null +++ b/ee/packages/den-db/src/schema/ui-artifacts.ts @@ -0,0 +1,16 @@ +import { sql } from "drizzle-orm" +import { boolean, json, mysqlTable, timestamp } from "drizzle-orm/mysql-core" +import type { UiArtifactKind } from "@openwork/types/ui-artifact" +import { denTypeIdColumn } from "../columns" + +export const UiArtifactPreferenceTable = mysqlTable( + "ui_artifact_preference", + { + memberId: denTypeIdColumn("member", "member_id").notNull().primaryKey(), + enabled: boolean("enabled").notNull().default(false), + enabledArtifactIds: json("enabled_artifact_ids").$type().notNull(), + updatedAt: timestamp("updated_at", { fsp: 3 }) + .notNull() + .default(sql`CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)`), + }, +) diff --git a/ee/packages/den-db/test/migration-readiness.test.ts b/ee/packages/den-db/test/migration-readiness.test.ts index c870b9f6fa..1251cc4541 100644 --- a/ee/packages/den-db/test/migration-readiness.test.ts +++ b/ee/packages/den-db/test/migration-readiness.test.ts @@ -69,6 +69,22 @@ describe("Den DB migration readiness wiring", () => { assert.ok(denDbBuildIndex < denApiBuildIndex, "den-db dist assets are built before den-api") }) + test("Dockerfile.den packages and builds every UI artifact MCP workspace dependency", () => { + const dockerfile = readRepoFile("packaging/docker/Dockerfile.den") + const publishWorkflow = readRepoFile(".github/workflows/publish-ee-images.yml") + const uiArtifactBuildIndex = dockerfile.indexOf("RUN pnpm --dir /app/packages/ui-artifact-mcp run build") + const denApiBuildIndex = dockerfile.indexOf("pnpm --dir /app/ee/apps/den-api run build") + + assert.match( + dockerfile, + /COPY packages\/ui-artifact-mcp\/package\.json \/app\/packages\/ui-artifact-mcp\/package\.json/, + ) + assert.match(dockerfile, /COPY packages\/ui-artifact-mcp \/app\/packages\/ui-artifact-mcp/) + assert.notEqual(uiArtifactBuildIndex, -1, "Dockerfile.den builds @openwork/ui-artifact-mcp") + assert.ok(uiArtifactBuildIndex < denApiBuildIndex, "ui-artifact-mcp is built before den-api") + assert.match(publishWorkflow, /- "packages\/ui-artifact-mcp\/\*\*"/) + }) + test("hosted Den API build includes den-db assets but start does not run migrations", () => { const denApiPackage = readRepoFile("ee/apps/den-api/package.json") const denApiBuild = readRepoFile("ee/apps/den-api/scripts/build.mjs") diff --git a/evals/flows/ui-artifacts-execute-capability.flow.mjs b/evals/flows/ui-artifacts-execute-capability.flow.mjs new file mode 100644 index 0000000000..b0ca6dbaa8 --- /dev/null +++ b/evals/flows/ui-artifacts-execute-capability.flow.mjs @@ -0,0 +1,738 @@ +import { mkdir } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + denApiFetch, + denApiUrl, + denWebUrl, + mcpAgentCall, + signInApi, +} from "./lib/den-web.mjs"; +import { loadVoiceoverParagraphs } from "../runner/voiceover.mjs"; + +const FLOW_ID = "ui-artifacts-execute-capability"; +const ARTIFACT_IDS = [ + "workspace.brief", + "calendar.view", + "widgets.collection", + "communication.thread", + "mail.inbox", + "work.attention", + "work.approvals", +]; +const SEARCH_CAPABILITY = "openwork.ui_artifacts.search"; +const USE_CAPABILITY = "openwork.ui_artifacts.use"; +const EMAIL = process.env.OPENWORK_EVAL_DEMO_EMAIL?.trim() || "alex@acme.test"; +const PASSWORD = process.env.OPENWORK_EVAL_DEMO_PASSWORD?.trim() || "OpenWorkDemo123!"; +const FIXTURE_WORKSPACE = join(tmpdir(), "openwork-ui-artifacts-eval"); +const vo = await loadVoiceoverParagraphs(FLOW_ID); + +const state = { + sessionToken: null, + organizationId: null, + mcpToken: null, + briefResult: null, + widgetsResult: null, + calendarDayResult: null, + calendarAgendaResult: null, + calendarWeekResult: null, + approvalResult: null, + approvalUpdatedResult: null, +}; + +function parseToolText(result) { + const text = result?.content?.find((entry) => entry?.type === "text")?.text; + return typeof text === "string" ? JSON.parse(text) : null; +} + +async function setUiArtifactPreferences(ctx, enabled) { + const preferences = await denApiFetch("/v1/me/ui-artifacts", { + method: "PUT", + headers: { + authorization: `Bearer ${state.sessionToken}`, + "x-openwork-legacy-org-id": state.organizationId, + }, + body: JSON.stringify({ enabled, enabledArtifactIds: ARTIFACT_IDS }), + }); + ctx.assert(preferences.response.ok, `Could not ${enabled ? "enable" : "disable"} UI artifacts: ${preferences.response.status}`); + ctx.assert(preferences.body.enabled === enabled, `Den did not persist the ${enabled ? "enabled" : "disabled"} artifact preference.`); +} + +async function prepareCloud(ctx) { + state.sessionToken = process.env.OPENWORK_EVAL_DEN_TOKEN?.trim() || await signInApi(EMAIL, PASSWORD); + ctx.assert(Boolean(state.sessionToken), `Could not sign in ${EMAIL} to the local Den.`); + + const orgs = await denApiFetch("/v1/me/orgs", { + headers: { authorization: `Bearer ${state.sessionToken}` }, + }); + ctx.assert(orgs.response.ok, `Could not list organizations: ${orgs.response.status}`); + state.organizationId = orgs.body.activeOrgId ?? orgs.body.orgs?.[0]?.id ?? null; + ctx.assert(Boolean(state.organizationId), "The signed-in member has no organization."); + + if (!orgs.body.activeOrgId) { + const active = await denApiFetch("/v1/me/active-organization", { + method: "POST", + headers: { authorization: `Bearer ${state.sessionToken}` }, + body: JSON.stringify({ organizationId: state.organizationId }), + }); + ctx.assert(active.response.ok, `Could not select the organization: ${active.response.status}`); + } + + await setUiArtifactPreferences(ctx, false); + + const minted = await denApiFetch("/v1/mcp/token", { + method: "POST", + headers: { + authorization: `Bearer ${state.sessionToken}`, + "x-openwork-legacy-org-id": state.organizationId, + }, + body: JSON.stringify({ scopes: ["mcp:read", "mcp:write"] }), + }); + ctx.assert(minted.response.ok && minted.body.token, `Could not mint the MCP token: ${minted.response.status}`); + state.mcpToken = minted.body.token; + return state.mcpToken; +} + +async function ensureSession(ctx) { + await ctx.waitFor("Boolean(window.__openworkControl)", { + timeoutMs: 60_000, + label: "OpenWork control API", + }); + if (await ctx.hasText("Continue without OpenWork Models")) { + await ctx.clickText("Continue without OpenWork Models", { selector: "button", timeoutMs: 10_000 }); + } + if (await ctx.hasText("Skip and use the free model")) { + await ctx.clickText("Skip and use the free model", { selector: "button", timeoutMs: 10_000 }); + } + const surveySkip = await ctx.eval(`Boolean([...document.querySelectorAll('button')] + .find((button) => button.textContent?.trim() === "Skip"))`); + if (surveySkip) await ctx.clickText("Skip", { selector: "button", timeoutMs: 10_000 }); + + const hasSession = await ctx.eval("window.__openworkControl.snapshot().route.includes('/session/')"); + if (!hasSession) { + let actionReady = await ctx.eval(`window.__openworkControl.listActions() + .some((action) => action.id === "session.create_task" && !action.disabled)`); + if (!actionReady) { + await mkdir(FIXTURE_WORKSPACE, { recursive: true }); + const welcomeInput = 'input[placeholder="/workspace/my-project"]'; + const onWelcome = await ctx.eval(`Boolean(document.querySelector(${JSON.stringify(welcomeInput)}))`); + if (onWelcome) { + await ctx.fill(welcomeInput, FIXTURE_WORKSPACE); + await ctx.clickText("Use this folder", { selector: "button", timeoutMs: 10_000 }); + await ctx.clickText("Continue without OpenWork Models", { selector: "button", timeoutMs: 30_000 }).catch(() => {}); + await ctx.clickText("Skip and use the free model", { selector: "button", timeoutMs: 30_000 }).catch(() => {}); + await ctx.clickText("Skip", { selector: "button", timeoutMs: 10_000 }).catch(() => {}); + } else { + await ctx.waitFor( + `window.__openworkControl.listActions() + .some((action) => action.id === "workspace.create" && !action.disabled)`, + { timeoutMs: 30_000, label: "workspace creation action" }, + ); + await ctx.control("workspace.create", { path: FIXTURE_WORKSPACE }); + } + await ctx.waitFor( + `window.__openworkControl.listActions() + .some((action) => action.id === "session.create_task" && !action.disabled)`, + { timeoutMs: 60_000, label: "task creation action" }, + ); + actionReady = true; + } + ctx.assert(actionReady, "A workspace session is required for the UI artifact eval."); + await ctx.control("session.create_task"); + await ctx.waitFor( + "window.__openworkControl.snapshot().route.includes('/session/')", + { timeoutMs: 60_000, label: "active session" }, + ); + } +} + +export default { + id: FLOW_ID, + title: "the opt-in execute_capability artifact lifecycle stays inert when off and renders when enabled", + requiredEnv: ["OPENWORK_EVAL_DEN_API_URL", "OPENWORK_EVAL_DEN_WEB_URL"], + steps: [ + { + name: "The alpha flag leaves the agent MCP surface inert when disabled", + run: async (ctx) => { + const mcpToken = await prepareCloud(ctx); + const initializedOff = await mcpAgentCall(mcpToken, "initialize", { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: FLOW_ID, version: "1.0.0" }, + }, ctx); + ctx.assert( + !initializedOff.instructions?.includes("uiArtifactSuggestions") + && !initializedOff.instructions?.includes("openwork.ui_artifacts"), + "Disabled UI artifact steering leaked into MCP initialization.", + ); + + const tools = await mcpAgentCall(mcpToken, "tools/list", {}, ctx); + const names = (tools.tools ?? []).map((tool) => tool.name).sort(); + ctx.assert( + names.join(",") === "execute_capability,search_capabilities", + `Unexpected disabled agent tool surface: ${names.join(", ")}`, + ); + + const searched = await mcpAgentCall(mcpToken, "tools/call", { + name: "search_capabilities", + arguments: { query: "artifact widget visual card", limit: 10 }, + }, ctx); + const disabledMatches = parseToolText(searched)?.matches ?? []; + ctx.assert( + disabledMatches.every((match) => !String(match.name).startsWith("openwork.ui_artifacts.")), + "Disabled UI artifact capabilities appeared in search results.", + ); + + const directUse = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: SEARCH_CAPABILITY, + body: { query: "calendar events today", limit: 1 }, + }, + }, ctx); + ctx.assert(directUse.isError === true, "A disabled artifact capability unexpectedly executed."); + ctx.assert(parseToolText(directUse)?.code === "artifact_disabled", "Disabled execution did not fail closed."); + + await setUiArtifactPreferences(ctx, true); + const initializedOn = await mcpAgentCall(mcpToken, "initialize", { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: FLOW_ID, version: "1.0.0" }, + }, ctx); + ctx.assert( + !initializedOn.instructions?.includes("uiArtifactSuggestions") + && !initializedOn.instructions?.includes("openwork.ui_artifacts"), + "Persistent UI artifact steering leaked into MCP initialization after opt-in.", + ); + }, + }, + { + name: "The cloud surface remains exactly two tools", + run: async (ctx) => { + const mcpToken = state.mcpToken; + ctx.assert(Boolean(mcpToken), "The MCP token was not prepared."); + const tools = await mcpAgentCall(mcpToken, "tools/list", {}, ctx); + const names = (tools.tools ?? []).map((tool) => tool.name).sort(); + ctx.assert( + names.join(",") === "execute_capability,search_capabilities", + `Unexpected agent tool surface: ${names.join(", ")}`, + ); + + const briefSearch = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: SEARCH_CAPABILITY, + body: { + query: "replace the full work home dashboard with one chat-native morning brief", + signal: { toolName: "openwork_today_summary" }, + limit: 1, + }, + }, + }, ctx); + ctx.assert(briefSearch.isError !== true, "The workspace brief search failed."); + const briefSearchResult = parseToolText(briefSearch); + const briefMatch = briefSearchResult?.matches?.[0]; + ctx.assert(briefMatch?.artifactId === "workspace.brief", "Artifact search did not rank the workspace brief first."); + ctx.assert( + briefMatch?.toolDefinition?.invocation?.capability === USE_CAPABILITY, + "Artifact search did not return the execute_capability use invocation.", + ); + + const briefRendered = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: USE_CAPABILITY, + body: briefMatch.toolDefinition.exampleArguments, + }, + }, ctx); + ctx.assert(briefRendered.isError !== true, "The workspace brief render failed."); + state.briefResult = parseToolText(briefRendered); + ctx.assert(state.briefResult?.status === "rendered", "The brief receipt is missing its rendered status."); + ctx.assert(state.briefResult?.artifact?.artifactId === "workspace.brief", "The render receipt is not a workspace brief."); + + const widgetsSearch = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: SEARCH_CAPABILITY, + body: { + query: "combine meetings, goal progress, service health, leave balance, and payroll into widgets", + signal: { toolName: "openwork_cross_source_widget_summary" }, + limit: 1, + }, + }, + }, ctx); + const widgetsMatch = parseToolText(widgetsSearch)?.matches?.[0]; + ctx.assert(widgetsMatch?.artifactId === "widgets.collection", "Artifact search did not return the widget collection."); + + const widgetsRendered = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: USE_CAPABILITY, + body: widgetsMatch.toolDefinition.exampleArguments, + }, + }, ctx); + state.widgetsResult = parseToolText(widgetsRendered); + ctx.assert(state.widgetsResult?.artifact?.artifactId === "widgets.collection", "The widgets receipt has the wrong artifact ID."); + ctx.assert( + new Set(state.widgetsResult?.artifact?.data?.widgets?.map((widget) => widget.kind)).size === 5, + "The widget collection did not retain all five widget kinds.", + ); + + const calendarSearch = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: SEARCH_CAPABILITY, + body: { + query: "show calendar events as day, agenda, or week", + signal: { toolName: "google_calendar_list_events" }, + limit: 1, + }, + }, + }, ctx); + const calendarMatch = parseToolText(calendarSearch)?.matches?.[0]; + ctx.assert(calendarMatch?.artifactId === "calendar.view", "Artifact search did not return the calendar."); + const calendarArguments = calendarMatch.toolDefinition.exampleArguments; + const calendarArtifact = calendarArguments.artifact; + const fridayEvent = { + ...calendarArtifact.data.events[0], + id: "weekly-planning", + title: "Weekly planning", + start: "2026-07-24T09:00:00+02:00", + end: "2026-07-24T09:45:00+02:00", + location: "Focus room", + action: undefined, + }; + const mondayEvent = { + ...calendarArtifact.data.events[0], + id: "design-review", + title: "Design review", + start: "2026-07-20T14:00:00+02:00", + end: "2026-07-20T15:00:00+02:00", + location: "Studio", + action: undefined, + }; + const calendarVariants = [ + { + key: "calendarDayResult", + instanceId: "demo-calendar-day", + title: "Today at a glance", + subtitle: "A focused daily timeline", + data: { + ...calendarArtifact.data, + variant: "day", + }, + }, + { + key: "calendarAgendaResult", + instanceId: "demo-calendar-agenda", + title: "Upcoming agenda", + subtitle: "Thursday and Friday", + data: { + ...calendarArtifact.data, + variant: "agenda", + endDate: "2026-07-24", + events: [...calendarArtifact.data.events, fridayEvent], + focusWindow: undefined, + }, + }, + { + key: "calendarWeekResult", + instanceId: "demo-calendar-week", + title: "This week", + subtitle: "Monday through Sunday", + data: { + ...calendarArtifact.data, + variant: "week", + startDate: "2026-07-20", + endDate: "2026-07-26", + events: [mondayEvent, ...calendarArtifact.data.events, fridayEvent], + focusWindow: undefined, + }, + }, + ]; + for (const variant of calendarVariants) { + const rendered = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: USE_CAPABILITY, + body: { + ...calendarArguments, + artifact: { + ...calendarArtifact, + instanceId: variant.instanceId, + title: variant.title, + subtitle: variant.subtitle, + data: variant.data, + }, + }, + }, + }, ctx); + ctx.assert(rendered.isError !== true, `The ${variant.data.variant} calendar render failed.`); + state[variant.key] = parseToolText(rendered); + ctx.assert( + state[variant.key]?.artifact?.data?.variant === variant.data.variant, + `The ${variant.data.variant} calendar receipt lost its variant.`, + ); + } + + const approvalSearch = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: SEARCH_CAPABILITY, + body: { query: "approvals waiting for my approve or reject decision", limit: 1 }, + }, + }, ctx); + const approvalMatch = parseToolText(approvalSearch)?.matches?.[0]; + ctx.assert(approvalMatch?.artifactId === "work.approvals", "Artifact search did not return the approval queue."); + + const approvalRendered = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: USE_CAPABILITY, + body: approvalMatch.toolDefinition.exampleArguments, + }, + }, ctx); + state.approvalResult = parseToolText(approvalRendered); + ctx.assert(state.approvalResult?.artifact?.revision === 1, "The mock approval queue did not start at revision 1."); + + const decisionBody = { + operation: "decide", + artifactId: "work.approvals", + instanceId: state.approvalResult.artifact.instanceId, + itemId: "expense-lisbon", + decision: "approve", + expectedRevision: 1, + }; + const approved = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { name: USE_CAPABILITY, body: decisionBody }, + }, ctx); + ctx.assert(approved.isError !== true, "The explicit mock approval decision failed."); + state.approvalUpdatedResult = parseToolText(approved); + ctx.assert(state.approvalUpdatedResult?.artifact?.revision === 2, "The mock approval queue did not advance to revision 2."); + ctx.assert( + state.approvalUpdatedResult?.artifact?.data?.items?.[0]?.status === "approved", + "The selected mock request was not approved.", + ); + + const stale = await mcpAgentCall(mcpToken, "tools/call", { + name: "execute_capability", + arguments: { + name: USE_CAPABILITY, + body: { ...decisionBody, itemId: "access-production", decision: "reject" }, + }, + }, ctx); + ctx.assert(stale.isError === true, "A stale approval decision unexpectedly succeeded."); + ctx.assert(parseToolText(stale)?.code === "revision_conflict", "The stale decision did not return revision_conflict."); + }, + }, + { + name: "The desktop shows the synced tile catalog and native card", + run: async (ctx) => { + await ensureSession(ctx); + await setUiArtifactPreferences(ctx, false); + const desktopBootstrap = { + baseUrl: denWebUrl(), + apiBaseUrl: denApiUrl(), + requireSignin: false, + handoff: null, + }; + const desktopSession = await ctx.eval(`(async () => { + const bridge = window.__OPENWORK_ELECTRON__?.invokeDesktop; + if (!bridge) return { ok: false, reason: "desktop bridge unavailable" }; + await bridge("setDesktopBootstrapConfig", ${JSON.stringify(desktopBootstrap)}); + localStorage.setItem("openwork.den.baseUrl", ${JSON.stringify(denWebUrl())}); + localStorage.setItem("openwork.den.authToken", ${JSON.stringify(state.sessionToken)}); + localStorage.setItem("openwork.den.activeOrgId", ${JSON.stringify(state.organizationId)}); + const current = JSON.parse(localStorage.getItem("openwork.preferences") || "{}"); + localStorage.setItem("openwork.preferences", JSON.stringify({ + ...current, + featureFlags: { ...(current.featureFlags || {}), uiArtifacts: false }, + uiArtifacts: { enabledArtifactIds: ${JSON.stringify(ARTIFACT_IDS)} }, + })); + window.location.reload(); + return { ok: true }; + })()`, { awaitPromise: true }); + ctx.assert(desktopSession?.ok, `Could not configure the desktop Den session: ${desktopSession?.reason ?? "unknown"}`); + await ensureSession(ctx); + await ctx.waitFor( + `!document.body.innerText.includes("OpenWork Cloud is temporarily unavailable.")`, + { timeoutMs: 30_000, label: "desktop Den session available" }, + ); + await ctx.waitFor( + `window.__openworkControl.listActions().some((action) => action.id === "eval.ui_artifact.seed_chat" && !action.disabled)`, + { timeoutMs: 20_000, label: "UI artifact eval action" }, + ); + + await ctx.prove("The default-off flag preserves the ordinary chat experience", { + voiceover: vo[0], + action: async () => { + await ctx.control("eval.ui_artifact.seed_chat", { result: state.widgetsResult }); + }, + assert: async () => { + const offState = await ctx.eval(`({ + railVisible: Boolean(document.querySelector('button[aria-label="UI Artifacts"]')), + nativeArtifactVisible: Boolean(document.querySelector('[data-ui-artifact-id]')), + })`); + ctx.assert(offState.railVisible === false, "The UI Artifacts rail entry was visible while disabled."); + ctx.assert(offState.nativeArtifactVisible === false, "A native UI artifact rendered while disabled."); + }, + screenshot: { + name: "ui-artifacts-disabled", + requireText: ["New session"], + rejectText: ["Something went wrong", "OpenWork Cloud is temporarily unavailable."], + }, + }); + + await setUiArtifactPreferences(ctx, true); + await ctx.eval(`(() => { + const current = JSON.parse(localStorage.getItem("openwork.preferences") || "{}"); + localStorage.setItem("openwork.preferences", JSON.stringify({ + ...current, + featureFlags: { ...(current.featureFlags || {}), uiArtifacts: true }, + uiArtifacts: { enabledArtifactIds: ${JSON.stringify(ARTIFACT_IDS)} }, + })); + window.location.reload(); + return true; + })()`); + await ensureSession(ctx); + await ctx.waitFor( + `Boolean(document.querySelector('button[aria-label="UI Artifacts"]'))`, + { timeoutMs: 30_000, label: "enabled UI Artifacts rail button" }, + ); + + await ctx.prove("The right rail offers seven member-controlled chat artifact tiles", { + voiceover: vo[1], + action: async () => { + await ctx.waitFor( + `Boolean(document.querySelector('button[aria-label="UI Artifacts"]'))`, + { timeoutMs: 30_000, label: "UI Artifacts rail button" }, + ); + await ctx.eval(`document.querySelector('button[aria-label="UI Artifacts"]')?.click()`); + }, + assert: async () => { + await ctx.expectText("7 of 7 standard artifacts enabled", { timeoutMs: 20_000 }); + await ctx.expectText("Workspace brief"); + await ctx.expectText("Calendar"); + await ctx.expectText("Widgets"); + await ctx.expectText("Priority inbox"); + await ctx.expectText("Approval queue"); + }, + screenshot: { + name: "ui-artifact-catalog", + requireText: ["7 of 7 standard artifacts enabled", "Workspace brief", "Calendar", "Widgets", "Approval queue"], + rejectText: ["Something went wrong", "OpenWork Cloud is temporarily unavailable."], + }, + }); + await ctx.eval(`document.querySelector('button[aria-label="Close UI artifacts"]')?.click()`); + await ctx.waitFor( + `window.__openworkControl.listActions().some((action) => action.id === "eval.ui_artifact.seed_chat" && !action.disabled)`, + { timeoutMs: 20_000, label: "enabled UI artifact eval action" }, + ); + + await ctx.prove("One widget artifact combines five independently typed widgets", { + voiceover: vo[2], + action: async () => { + await ctx.control("eval.ui_artifact.seed_chat", { result: state.widgetsResult }); + await ctx.waitFor( + `Boolean(document.querySelector('[data-ui-artifact-id="widgets.collection"]'))`, + { timeoutMs: 20_000, label: "native widget collection artifact card" }, + ); + await ctx.eval(`document.querySelector('[data-ui-artifact-id="widgets.collection"]')?.scrollIntoView({ block: "center" })`); + }, + assert: async () => { + await ctx.expectText("Your widgets"); + await ctx.expectText("Meetings"); + await ctx.expectText("Q3 goals"); + await ctx.expectText("Payment service"); + await ctx.expectText("Leave balance"); + await ctx.expectText("Next payslip"); + const widgetKinds = await ctx.eval( + `[...document.querySelectorAll('[data-ui-artifact-id="widgets.collection"] [class*="capitalize"]')] + .map((element) => element.textContent?.trim()) + .filter(Boolean)`, + ); + for (const kind of ["metric", "progress", "status", "balance", "date"]) { + ctx.assert(widgetKinds.includes(kind), `The combined widget artifact did not render the ${kind} widget.`); + } + }, + screenshot: { + name: "composable-widget-collection", + requireText: ["Your widgets", "Meetings", "Q3 goals", "Payment service", "Leave balance", "Next payslip"], + rejectText: ["Something went wrong", "UI artifact renderer unavailable", "OpenWork Cloud is temporarily unavailable."], + }, + }); + + await ctx.prove("The calendar day variant emphasizes one chronological timeline and focus window", { + voiceover: vo[3], + action: async () => { + await ctx.control("eval.ui_artifact.seed_chat", { result: state.calendarDayResult }); + await ctx.waitFor( + `Boolean(document.querySelector('[data-ui-artifact-id="calendar.view"]'))`, + { timeoutMs: 20_000, label: "native day calendar artifact card" }, + ); + await ctx.eval(`document.querySelector('[data-ui-artifact-id="calendar.view"]')?.scrollIntoView({ block: "center" })`); + }, + assert: async () => { + await ctx.expectText("Today at a glance"); + await ctx.expectText("Day View"); + await ctx.expectText("Architecture review"); + await ctx.expectText("Best focus window"); + }, + screenshot: { + name: "calendar-day-variant", + requireText: ["Today at a glance", "Day View", "Architecture review", "Best focus window"], + rejectText: ["Something went wrong", "UI artifact renderer unavailable", "OpenWork Cloud is temporarily unavailable."], + }, + }); + + await ctx.prove("The same calendar artifact switches to a grouped multi-day agenda", { + voiceover: vo[4], + action: async () => { + await ctx.control("eval.ui_artifact.seed_chat", { result: state.calendarAgendaResult }); + await ctx.waitFor( + `document.querySelector('[data-ui-artifact-id="calendar.view"]')?.innerText?.includes("Agenda View")`, + { timeoutMs: 20_000, label: "native agenda calendar variant" }, + ); + await ctx.eval(`document.querySelector('[data-ui-artifact-id="calendar.view"]')?.scrollIntoView({ block: "center" })`); + }, + assert: async () => { + await ctx.expectText("Upcoming agenda"); + await ctx.expectText("Agenda View"); + await ctx.expectText("Architecture review"); + await ctx.expectText("Weekly planning"); + await ctx.expectNoText("Best focus window"); + }, + screenshot: { + name: "calendar-agenda-variant", + requireText: ["Upcoming agenda", "Agenda View", "Architecture review", "Weekly planning"], + rejectText: ["Best focus window", "Something went wrong", "UI artifact renderer unavailable"], + }, + }); + + await ctx.prove("The calendar week variant groups the same event contract by date", { + voiceover: vo[5], + action: async () => { + await ctx.control("eval.ui_artifact.seed_chat", { result: state.calendarWeekResult }); + await ctx.waitFor( + `document.querySelector('[data-ui-artifact-id="calendar.view"]')?.innerText?.includes("Week View")`, + { timeoutMs: 20_000, label: "native week calendar variant" }, + ); + await ctx.eval(`document.querySelector('[data-ui-artifact-id="calendar.view"]')?.scrollIntoView({ block: "center" })`); + }, + assert: async () => { + await ctx.expectText("This week"); + await ctx.expectText("Week View"); + await ctx.expectText("Design review"); + await ctx.expectText("Architecture review"); + await ctx.expectText("Weekly planning"); + }, + screenshot: { + name: "calendar-week-variant", + requireText: ["This week", "Week View", "Design review", "Architecture review", "Weekly planning"], + rejectText: ["Something went wrong", "UI artifact renderer unavailable", "OpenWork Cloud is temporarily unavailable."], + }, + }); + + await ctx.prove("A single workspace brief replaces the screenshot-style home dashboard inside chat", { + voiceover: vo[6], + action: async () => { + await ctx.control("eval.ui_artifact.seed_chat", { result: state.briefResult }); + await ctx.waitFor( + `Boolean(document.querySelector('[data-ui-artifact-id="workspace.brief"]'))`, + { timeoutMs: 20_000, label: "native workspace brief artifact card" }, + ); + await ctx.eval(`document.querySelector('[data-ui-artifact-id="workspace.brief"]')?.scrollIntoView({ block: "center" })`); + }, + assert: async () => { + await ctx.expectText("Good morning, Alex"); + await ctx.expectText("Today at a glance"); + await ctx.expectText("Needs your attention"); + await ctx.expectText("Your widgets"); + await ctx.expectText("Architecture review"); + await ctx.expectText("Demo data"); + }, + screenshot: { + name: "workspace-brief-in-chat", + requireText: ["Good morning, Alex", "Today at a glance", "Needs your attention", "Your widgets", "Demo data"], + rejectText: ["Something went wrong", "UI artifact renderer unavailable", "OpenWork Cloud is temporarily unavailable."], + }, + }); + + await ctx.prove("The approval artifact starts at revision one without making a decision", { + voiceover: vo[7], + action: async () => { + await ctx.control("eval.ui_artifact.seed_chat", { result: state.approvalResult }); + await ctx.waitFor( + `Boolean(document.querySelector('[data-ui-artifact-id="work.approvals"]'))`, + { timeoutMs: 20_000, label: "native approval queue artifact card" }, + ); + await ctx.eval(`document.querySelector('[data-ui-artifact-id="work.approvals"]')?.scrollIntoView({ block: "center" })`); + }, + assert: async () => { + await ctx.expectText("Customer workshop travel"); + await ctx.expectText("Mock state · revision 1"); + await ctx.expectText("Approve"); + await ctx.expectText("Reject"); + }, + screenshot: { + name: "approval-awaiting-decision", + requireText: ["Approvals", "Customer workshop travel", "Mock state · revision 1", "Approve", "Reject"], + rejectText: ["Something went wrong", "OpenWork Cloud is temporarily unavailable."], + }, + }); + + await ctx.prove("Approval stays user-controlled by staging a minimal revision-bound prompt", { + voiceover: vo[8], + action: async () => { + await ctx.clickText("Approve", { selector: "button", timeoutMs: 10_000 }); + await ctx.waitFor( + `document.querySelector("[contenteditable='true']")?.textContent?.includes("openwork.ui_artifacts.use")`, + { timeoutMs: 10_000, label: "minimal approval prompt staged in composer" }, + ); + const stagedPrompt = await ctx.eval(`document.querySelector("[contenteditable='true']")?.textContent || ""`); + ctx.assert(stagedPrompt.includes('"expectedRevision":1'), "The staged approval prompt omitted the expected revision."); + ctx.assert(!stagedPrompt.includes("Flights and two hotel nights"), "The staged approval prompt copied unnecessary artifact detail."); + ctx.assert(stagedPrompt.includes("Do not call a provider approval tool"), "The staged approval prompt omitted the mock-only boundary."); + }, + assert: async () => { + await ctx.expectText("Mock state · revision 1"); + await ctx.expectText("Do not call a provider approval tool"); + }, + screenshot: { + name: "approval-prompt-staged", + requireText: ["Approvals", "Mock state · revision 1", "openwork.ui_artifacts.use", "Do not call a provider approval tool"], + rejectText: ["Something went wrong", "OpenWork Cloud is temporarily unavailable."], + }, + }); + + await ctx.prove("The successful execute response replaces the card with revision two while stale replay is rejected", { + voiceover: vo[9], + action: async () => { + await ctx.control("eval.ui_artifact.seed_chat", { + result: state.approvalUpdatedResult, + clearPrompt: true, + }); + await ctx.eval(`document.querySelector('[data-ui-artifact-id="work.approvals"]')?.scrollIntoView({ block: "center" })`); + }, + assert: async () => { + await ctx.expectText("Updated mock state · revision 2", { timeoutMs: 20_000 }); + await ctx.expectText("approved"); + const composerText = await ctx.eval(`document.querySelector("[contenteditable='true']")?.textContent?.trim() || ""`); + ctx.assert(composerText === "", "The submitted approval prompt remained in the composer."); + const renderedApprovalCards = await ctx.eval( + `document.querySelectorAll('[data-ui-artifact-id="work.approvals"]').length`, + ); + ctx.assert(renderedApprovalCards === 1, `Expected one reconciled approval card, found ${renderedApprovalCards}.`); + }, + screenshot: { + name: "approval-revision-updated", + requireText: ["Approvals", "approved", "Updated mock state · revision 2"], + rejectText: ["Something went wrong", "OpenWork Cloud is temporarily unavailable."], + }, + }); + await ctx.expectNoText("Something went wrong"); + }, + }, + ], +}; diff --git a/evals/voiceovers/ui-artifacts-execute-capability.md b/evals/voiceovers/ui-artifacts-execute-capability.md new file mode 100644 index 0000000000..7bff2d3240 --- /dev/null +++ b/evals/voiceovers/ui-artifacts-execute-capability.md @@ -0,0 +1,23 @@ +# UI artifacts — an execute-capability lifecycle + +This demo follows deterministic mock data through the same two-tool cloud surface available to any compatible agent engine. It first proves the alpha is inert while disabled, then shows artifact discovery, composable widget rendering, multiple calendar presentations, explicit approval, state replacement, and stale-revision protection without contacting a live provider. + +1. UI Artifacts is off by default. Agent initialization never carries persistent artifact steering; enabled suggestions inject bounded guidance only into that turn. With the member preference disabled, artifact capabilities cannot be discovered or executed, the right-rail entry is absent, and an artifact-shaped tool receipt stays in the ordinary chat renderer. + +2. After the member opts in, UI Artifacts appears in the right rail. The member can inspect seven standard chat-native patterns, including a variant-driven Calendar and a composable Widgets collection, then decide which ones the agent is allowed to suggest and render. + +3. Widgets is one artifact, not a growing set of one-off cards. This example combines a metric, progress, service status, leave balance, and payroll date in a single grid. The schema also supports strip and stack layouts, so the agent can compose only the widgets relevant to the answer. + +4. Calendar is also one artifact with variants. The day view prioritizes a chronological timeline and keeps the best focus window visible beneath today’s meetings. + +5. With the same calendar event contract, the agenda variant groups events across a date range. It removes day-only focus guidance and makes the next meeting across multiple days easy to scan. + +6. The week variant changes the layout again, grouping the same event objects into compact date cards. The agent chooses a variant through the searched schema instead of discovering a new tool for every presentation. + +7. Behind the scenes, the agent still kept OpenWork Cloud to search capabilities and execute capability. A workspace brief can remain a higher-level answer for broad requests while Calendar and Widgets stay independently reusable for focused requests. + +8. An approval is rendered as mock data at revision one with both choices visible. Nothing has changed yet: the card is waiting for the member to make an explicit decision. + +9. Clicking Approve still does not execute anything silently. It stages a visible, minimal request containing only the artifact instance, selected item, decision, and expected revision, while clearly forbidding a live provider action. + +10. Once that exact request is executed, the same artifact instance is replaced by revision two and the selected item becomes approved. The mock MCP has also rejected a stale revision-one replay, proving that an old card cannot overwrite newer state. diff --git a/packages/types/package.json b/packages/types/package.json index d4e6e9fa5b..8a3f558c72 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -74,6 +74,11 @@ "development": "./src/desktop-ipc.ts", "default": "./src/desktop-ipc.ts" }, + "./ui-artifact": { + "types": "./src/ui-artifact.ts", + "development": "./src/ui-artifact.ts", + "default": "./src/ui-artifact.ts" + }, "./connect-link": { "types": "./src/connect-link.ts", "development": "./src/connect-link.ts", diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f30dc9bab4..a3c9c01952 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -8,3 +8,4 @@ export * from "./den/egress-diagnostics" export * from "./den/inference" export * from "./den/microsoft-365" export * from "./den/mcp-connection-action" +export * from "./ui-artifact" diff --git a/packages/types/src/openwork-context.ts b/packages/types/src/openwork-context.ts index e33c44f94b..c5fd7fd4ab 100644 --- a/packages/types/src/openwork-context.ts +++ b/packages/types/src/openwork-context.ts @@ -91,7 +91,7 @@ export const openworkContextSnapshotSchema = z.object({ sidePanel: z.object({ open: z.boolean(), ownerSessionId: z.string().nullable(), - kind: z.enum(["panel", "extensions", "voice"]).nullable(), + kind: z.enum(["panel", "extensions", "ui-artifacts", "voice"]).nullable(), tabs: z.array(openworkPanelTabSchema), activeTabId: z.string().nullable(), }), diff --git a/packages/types/src/ui-artifact.ts b/packages/types/src/ui-artifact.ts new file mode 100644 index 0000000000..c3f901b81a --- /dev/null +++ b/packages/types/src/ui-artifact.ts @@ -0,0 +1,507 @@ +import { z } from "zod" + +export const UI_ARTIFACT_PROTOCOL = "openwork.ui-artifact" +export const UI_ARTIFACT_SCHEMA_VERSION = "1" +export const UI_ARTIFACT_MAX_JSON_BYTES = 40_000 +export const UI_ARTIFACT_SEARCH_CAPABILITY = "openwork.ui_artifacts.search" +export const UI_ARTIFACT_RENDER_CAPABILITY = "openwork.ui_artifacts.render" +export const UI_ARTIFACT_USE_CAPABILITY = "openwork.ui_artifacts.use" + +export const UI_ARTIFACT_KINDS = [ + "workspace.brief", + "calendar.view", + "widgets.collection", + "communication.thread", + "mail.inbox", + "work.attention", + "work.approvals", +] as const + +export const uiArtifactKindSchema = z.enum(UI_ARTIFACT_KINDS) +export type UiArtifactKind = z.infer + +export const uiArtifactPreferencesSchema = z.object({ + protocol: z.literal("openwork.ui-artifact-preferences"), + schemaVersion: z.literal(UI_ARTIFACT_SCHEMA_VERSION), + enabled: z.boolean(), + enabledArtifactIds: z.array(uiArtifactKindSchema).max(UI_ARTIFACT_KINDS.length), + updatedAt: z.string().datetime({ offset: true }).nullable(), +}).strict() +export type UiArtifactPreferences = z.infer + +export const uiArtifactPreferencesUpdateSchema = z.object({ + enabled: z.boolean(), + enabledArtifactIds: z.array(uiArtifactKindSchema).max(UI_ARTIFACT_KINDS.length), +}).strict() +export type UiArtifactPreferencesUpdate = z.infer + +const compactTextSchema = z.string().trim().min(1).max(160) +const detailTextSchema = z.string().trim().min(1).max(2_000) +const isoDateTimeSchema = z.string().datetime({ offset: true }) +const webUrlSchema = z.string().url().refine((value) => { + try { + const url = new URL(value) + return url.protocol === "https:" && !url.username && !url.password + } catch { + return false + } +}, "Only credential-free https URLs are supported") + +export const uiArtifactOpenUrlActionSchema = z.object({ + id: compactTextSchema, + label: compactTextSchema, + type: z.literal("open_url"), + url: webUrlSchema, + description: compactTextSchema.optional(), +}).strict() +export const uiArtifactDecisionActionSchema = z.object({ + id: compactTextSchema, + label: compactTextSchema, + type: z.literal("request_decision"), + instanceId: z.string().trim().min(1).max(128), + itemId: compactTextSchema, + decision: z.enum(["approve", "reject"]), + expectedRevision: z.number().int().positive(), + description: compactTextSchema.optional(), +}).strict() +export const uiArtifactActionSchema = z.discriminatedUnion("type", [ + uiArtifactOpenUrlActionSchema, + uiArtifactDecisionActionSchema, +]) +export type UiArtifactAction = z.infer + +export const uiArtifactSourceSchema = z.object({ + type: z.enum(["mock", "provider", "derived"]), + label: compactTextSchema, + provider: compactTextSchema.optional(), + account: compactTextSchema.optional(), + observedAt: isoDateTimeSchema.optional(), +}).strict() +export type UiArtifactSource = z.infer + +export const uiArtifactPresentationSchema = z.object({ + placement: z.enum(["inline", "panel", "both"]), + size: z.enum(["compact", "standard", "expanded"]), +}).strict() +export type UiArtifactPresentation = z.infer + +const calendarDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/) +const uiArtifactCalendarEventSchema = z.object({ + id: compactTextSchema, + title: compactTextSchema, + start: isoDateTimeSchema, + end: isoDateTimeSchema, + allDay: z.boolean().optional(), + location: compactTextSchema.optional(), + calendar: compactTextSchema.optional(), + status: z.enum(["confirmed", "tentative", "cancelled"]).optional(), + action: uiArtifactOpenUrlActionSchema.optional(), +}).strict() +const uiArtifactFocusWindowSchema = z.object({ + start: isoDateTimeSchema, + end: isoDateTimeSchema, + label: compactTextSchema, +}).strict() + +export const uiArtifactCalendarVariantSchema = z.enum(["day", "agenda", "week"]) +export type UiArtifactCalendarVariant = z.infer + +export const uiArtifactCalendarDataSchema = z.object({ + variant: uiArtifactCalendarVariantSchema + .describe("Use day for a single-day timeline, agenda for a chronological range, or week for date-grouped events."), + startDate: calendarDateSchema, + endDate: calendarDateSchema, + timezone: compactTextSchema, + events: z.array(uiArtifactCalendarEventSchema).max(50), + focusWindow: uiArtifactFocusWindowSchema.optional(), + action: uiArtifactOpenUrlActionSchema.optional(), +}).strict() +export type UiArtifactCalendarData = z.infer + +export const uiArtifactCommunicationThreadDataSchema = z.object({ + workspace: compactTextSchema, + channel: compactTextSchema, + topic: compactTextSchema.optional(), + unreadCount: z.number().int().nonnegative().optional(), + messages: z.array(z.object({ + id: compactTextSchema, + author: compactTextSchema, + timestamp: isoDateTimeSchema, + body: detailTextSchema, + reactions: z.array(z.object({ + emoji: compactTextSchema, + count: z.number().int().positive(), + }).strict()).max(12).optional(), + }).strict()).max(30), + action: uiArtifactOpenUrlActionSchema.optional(), +}).strict() +export type UiArtifactCommunicationThreadData = z.infer + +export const uiArtifactMailInboxDataSchema = z.object({ + account: compactTextSchema, + folder: compactTextSchema, + unreadCount: z.number().int().nonnegative(), + messages: z.array(z.object({ + id: compactTextSchema, + sender: compactTextSchema, + senderEmail: z.string().email().optional(), + subject: compactTextSchema, + snippet: detailTextSchema, + receivedAt: isoDateTimeSchema, + unread: z.boolean(), + labels: z.array(compactTextSchema).max(8).optional(), + action: uiArtifactOpenUrlActionSchema.optional(), + }).strict()).max(50), + action: uiArtifactOpenUrlActionSchema.optional(), +}).strict() +export type UiArtifactMailInboxData = z.infer + +export const uiArtifactAttentionDataSchema = z.object({ + items: z.array(z.object({ + id: compactTextSchema, + kind: z.enum(["incident", "approval", "task", "goal", "learning"]), + title: compactTextSchema, + description: detailTextSchema.optional(), + priority: z.enum(["low", "normal", "high", "critical"]), + source: compactTextSchema.optional(), + dueAt: isoDateTimeSchema.optional(), + action: uiArtifactOpenUrlActionSchema.optional(), + }).strict()).max(50), +}).strict() +export type UiArtifactAttentionData = z.infer + +export const uiArtifactWidgetToneSchema = z.enum(["neutral", "info", "success", "warning", "critical"]) +const uiArtifactWidgetBaseShape = { + id: compactTextSchema, + label: compactTextSchema, + detail: compactTextSchema.optional(), + tone: uiArtifactWidgetToneSchema, + action: uiArtifactOpenUrlActionSchema.optional(), +} + +export const uiArtifactMetricWidgetSchema = z.object({ + ...uiArtifactWidgetBaseShape, + kind: z.literal("metric"), + value: compactTextSchema, + trend: z.object({ + direction: z.enum(["up", "down", "flat"]), + label: compactTextSchema, + }).strict().optional(), +}).strict() + +export const uiArtifactProgressWidgetSchema = z.object({ + ...uiArtifactWidgetBaseShape, + kind: z.literal("progress"), + value: compactTextSchema, + progress: z.number().min(0).max(100), +}).strict() + +export const uiArtifactStatusWidgetSchema = z.object({ + ...uiArtifactWidgetBaseShape, + kind: z.literal("status"), + value: compactTextSchema, + status: z.enum(["healthy", "attention", "blocked", "offline"]), +}).strict() + +export const uiArtifactBalanceWidgetSchema = z.object({ + ...uiArtifactWidgetBaseShape, + kind: z.literal("balance"), + value: compactTextSchema, + unit: compactTextSchema.optional(), +}).strict() + +export const uiArtifactDateWidgetSchema = z.object({ + ...uiArtifactWidgetBaseShape, + kind: z.literal("date"), + value: compactTextSchema, + timestamp: isoDateTimeSchema.optional(), +}).strict() + +export const uiArtifactWidgetSchema = z.discriminatedUnion("kind", [ + uiArtifactMetricWidgetSchema, + uiArtifactProgressWidgetSchema, + uiArtifactStatusWidgetSchema, + uiArtifactBalanceWidgetSchema, + uiArtifactDateWidgetSchema, +]) +export type UiArtifactWidget = z.infer + +export const uiArtifactWidgetsDataSchema = z.object({ + layout: z.enum(["grid", "strip", "stack"]), + widgets: z.array(uiArtifactWidgetSchema).min(1).max(12) + .describe("A composable list of independently typed widgets rendered together in one artifact."), +}).strict() +export type UiArtifactWidgetsData = z.infer + +const uiArtifactSummaryMetricSchema = z.object({ + id: compactTextSchema, + label: compactTextSchema, + value: compactTextSchema, + detail: compactTextSchema.optional(), + tone: uiArtifactWidgetToneSchema, +}).strict() +const uiArtifactProgressSummarySchema = z.object({ + id: compactTextSchema, + label: compactTextSchema, + value: compactTextSchema, + detail: compactTextSchema.optional(), + progress: z.number().min(0).max(100).optional(), + tone: uiArtifactWidgetToneSchema, + action: uiArtifactOpenUrlActionSchema.optional(), +}).strict() + +export const uiArtifactApprovalsDataSchema = z.object({ + items: z.array(z.object({ + id: compactTextSchema, + title: compactTextSchema, + description: detailTextSchema.optional(), + requestor: compactTextSchema, + submittedAt: isoDateTimeSchema, + dueAt: isoDateTimeSchema.optional(), + amount: compactTextSchema.optional(), + source: compactTextSchema, + status: z.enum(["pending", "approved", "rejected"]), + decidedAt: isoDateTimeSchema.optional(), + decisionNote: compactTextSchema.optional(), + actions: z.array(uiArtifactDecisionActionSchema).max(2).optional(), + }).strict()).max(30), +}).strict() +export type UiArtifactApprovalsData = z.infer + +export const uiArtifactWorkspaceBriefDataSchema = z.object({ + summary: detailTextSchema, + metrics: z.array(uiArtifactSummaryMetricSchema).min(1).max(8), + schedule: z.array(uiArtifactCalendarEventSchema).max(8), + attention: uiArtifactAttentionDataSchema.shape.items.max(8), + progress: z.array(uiArtifactProgressSummarySchema).max(8), + quickActions: z.array(uiArtifactOpenUrlActionSchema).max(6), +}).strict() +export type UiArtifactWorkspaceBriefData = z.infer + +const uiArtifactBaseShape = { + instanceId: z.string().trim().min(1).max(128), + revision: z.number().int().positive(), + operation: z.enum(["create", "replace"]), + title: compactTextSchema, + subtitle: compactTextSchema.optional(), + presentation: uiArtifactPresentationSchema, + source: uiArtifactSourceSchema, +} + +export const uiArtifactCalendarSchema = z.object({ + ...uiArtifactBaseShape, + artifactId: z.literal("calendar.view"), + data: uiArtifactCalendarDataSchema, +}).strict() + +export const uiArtifactWidgetsSchema = z.object({ + ...uiArtifactBaseShape, + artifactId: z.literal("widgets.collection"), + data: uiArtifactWidgetsDataSchema, +}).strict() + +export const uiArtifactCommunicationThreadSchema = z.object({ + ...uiArtifactBaseShape, + artifactId: z.literal("communication.thread"), + data: uiArtifactCommunicationThreadDataSchema, +}).strict() + +export const uiArtifactMailInboxSchema = z.object({ + ...uiArtifactBaseShape, + artifactId: z.literal("mail.inbox"), + data: uiArtifactMailInboxDataSchema, +}).strict() + +export const uiArtifactAttentionSchema = z.object({ + ...uiArtifactBaseShape, + artifactId: z.literal("work.attention"), + data: uiArtifactAttentionDataSchema, +}).strict() + +export const uiArtifactApprovalsSchema = z.object({ + ...uiArtifactBaseShape, + artifactId: z.literal("work.approvals"), + data: uiArtifactApprovalsDataSchema, +}).strict() + +export const uiArtifactWorkspaceBriefSchema = z.object({ + ...uiArtifactBaseShape, + artifactId: z.literal("workspace.brief"), + data: uiArtifactWorkspaceBriefDataSchema, +}).strict() + +export const uiArtifactPayloadSchema = z.discriminatedUnion("artifactId", [ + uiArtifactWorkspaceBriefSchema, + uiArtifactCalendarSchema, + uiArtifactWidgetsSchema, + uiArtifactCommunicationThreadSchema, + uiArtifactMailInboxSchema, + uiArtifactAttentionSchema, + uiArtifactApprovalsSchema, +]) +export type UiArtifactPayload = z.infer + +export const uiArtifactSchemaDigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/) +export type UiArtifactSchemaDigest = z.infer + +/** + * The always-registered render tool deliberately keeps `artifact` unknown. + * `search_artifacts` returns the selected strict schema and digest; the render + * runtime resolves that definition and validates the payload before rendering. + */ +export const uiArtifactRenderInputSchema = z.object({ + artifactId: uiArtifactKindSchema, + artifactVersion: z.literal(UI_ARTIFACT_SCHEMA_VERSION), + schemaDigest: uiArtifactSchemaDigestSchema, + artifact: z.unknown(), +}).strict() +export type UiArtifactRenderInput = z.infer + +export const uiArtifactDecisionInputSchema = z.object({ + operation: z.literal("decide"), + artifactId: z.literal("work.approvals"), + instanceId: z.string().trim().min(1).max(128), + itemId: compactTextSchema, + decision: z.enum(["approve", "reject"]), + expectedRevision: z.number().int().positive(), + note: compactTextSchema.optional(), +}).strict() +export type UiArtifactDecisionInput = z.infer + +export const uiArtifactUseInputSchema = z.union([ + uiArtifactRenderInputSchema, + uiArtifactDecisionInputSchema, +]) +export type UiArtifactUseInput = z.infer + +export const uiArtifactSearchInputSchema = z.object({ + query: z.string().trim().min(1).max(500) + .describe("What the user wants to see, such as 'today's calendar' or 'Slack launch thread'."), + signal: z.object({ + toolName: z.string().trim().min(1).max(200), + toolTitle: z.string().trim().min(1).max(300).optional(), + toolDescription: z.string().trim().min(1).max(1_000).optional(), + arguments: z.record(z.string(), z.unknown()).optional(), + }).strict().optional().describe("Optional metadata from the data tool that triggered artifact discovery."), + enabledArtifactIds: z.array(uiArtifactKindSchema).max(UI_ARTIFACT_KINDS.length).optional() + .describe("Limit results to the UI artifact kinds enabled by the user."), + limit: z.number().int().min(1).max(5).default(3), +}).strict() +export type UiArtifactSearchInput = z.infer + +export const uiArtifactToolDefinitionSchema = z.object({ + name: z.enum(["use_artifact", "render_artifact", "execute_capability"]), + title: z.string(), + description: z.string(), + artifactVersion: z.literal(UI_ARTIFACT_SCHEMA_VERSION), + schemaDigest: uiArtifactSchemaDigestSchema, + inputSchema: z.record(z.string(), z.unknown()), + invocation: z.discriminatedUnion("toolName", [ + z.object({ + toolName: z.enum(["use_artifact", "render_artifact"]), + argumentsField: z.literal("artifact"), + }).strict(), + z.object({ + toolName: z.literal("execute_capability"), + capability: z.enum([UI_ARTIFACT_USE_CAPABILITY, UI_ARTIFACT_RENDER_CAPABILITY]), + argumentsField: z.literal("body"), + }).strict(), + ]), + exampleArguments: uiArtifactRenderInputSchema, +}).strict() +export type UiArtifactToolDefinition = z.infer + +export const uiArtifactSearchResultSchema = z.object({ + protocol: z.literal("openwork.ui-artifact-search"), + schemaVersion: z.literal(UI_ARTIFACT_SCHEMA_VERSION), + query: z.string(), + matches: z.array(z.object({ + artifactId: uiArtifactKindSchema, + title: z.string(), + description: z.string(), + score: z.number().int().nonnegative(), + reasons: z.array(z.string()), + toolDefinition: uiArtifactToolDefinitionSchema, + }).strict()), +}).strict() +export type UiArtifactSearchResult = z.infer + +export const uiArtifactSuggestionEnvelopeSchema = z.object({ + protocol: z.literal("openwork.ui-artifact-suggestions"), + schemaVersion: z.literal(UI_ARTIFACT_SCHEMA_VERSION), + agentInstruction: z.string().trim().min(1).max(1_200), + trigger: z.object({ + capability: z.string().trim().min(1).max(500), + }).strict(), + contextPolicy: z.object({ + selection: z.literal("optional"), + maxRendersThisTurn: z.literal(1), + expires: z.literal("end_of_turn"), + dedupeKey: compactTextSchema, + includesSourceValues: z.literal(false), + }).strict(), + suggestions: z.array(z.object({ + artifactId: uiArtifactKindSchema, + title: z.string().trim().min(1).max(160), + reason: z.string().trim().min(1).max(300), + invocation: z.object({ + toolName: z.literal("execute_capability"), + arguments: z.object({ + name: z.literal(UI_ARTIFACT_SEARCH_CAPABILITY), + body: uiArtifactSearchInputSchema.omit({ enabledArtifactIds: true }), + }).strict(), + }).strict(), + }).strict()).max(3), +}).strict() +export type UiArtifactSuggestionEnvelope = z.infer + +export const uiArtifactRenderResultSchema = z.object({ + protocol: z.literal(UI_ARTIFACT_PROTOCOL), + schemaVersion: z.literal(UI_ARTIFACT_SCHEMA_VERSION), + status: z.literal("rendered"), + artifact: uiArtifactPayloadSchema, + narration: z.object({ + summary: detailTextSchema, + visibleFacts: z.array(compactTextSchema).max(8), + }).strict(), + interaction: z.object({ + type: z.literal("decision"), + itemId: compactTextSchema, + decision: z.enum(["approve", "reject"]), + previousRevision: z.number().int().positive(), + revision: z.number().int().positive(), + }).strict().optional(), +}).strict() +export type UiArtifactRenderResult = z.infer + +export const uiArtifactErrorCodeSchema = z.enum([ + "invalid_search_input", + "unknown_artifact", + "artifact_disabled", + "renderer_unsupported", + "schema_digest_mismatch", + "invalid_artifact_payload", + "payload_too_large", + "unsafe_action", + "source_receipt_required", + "source_receipt_invalid", + "manifest_changed", + "operation_unsupported", + "revision_conflict", + "state_not_found", + "action_not_allowed", + "internal_error", +]) +export type UiArtifactErrorCode = z.infer + +export const uiArtifactErrorSchema = z.object({ + protocol: z.literal("openwork.ui-artifact-error"), + schemaVersion: z.literal(UI_ARTIFACT_SCHEMA_VERSION), + code: uiArtifactErrorCodeSchema, + message: z.string().trim().min(1).max(500), + retry: z.object({ + action: z.enum(["search_artifacts", "use_artifact", "render_artifact", "refresh_source", "none"]), + changedArgumentsRequired: z.boolean(), + }).strict(), +}).strict() +export type UiArtifactError = z.infer diff --git a/packages/ui-artifact-mcp/README.md b/packages/ui-artifact-mcp/README.md new file mode 100644 index 0000000000..f3a5f1a7fa --- /dev/null +++ b/packages/ui-artifact-mcp/README.md @@ -0,0 +1,109 @@ +# OpenWork UI Artifact MCP + +Deterministic local MCP server for developing chat-native OpenWork UI artifacts +without provider credentials or external data. + +It is a real stateful stdio MCP and exposes exactly two tools: + +- `search_artifacts` ranks the enabled artifact catalog from a user query and + optional triggering tool metadata. +- `use_artifact` validates the exact artifact schema/version/digest returned by + search, renders it, and retains mock instance state for the server process. + It also applies explicit revision-safe approval or rejection decisions. + +The mock catalog currently includes: + +- `workspace.brief` +- `calendar.view` +- `widgets.collection` +- `communication.thread` +- `mail.inbox` +- `work.attention` +- `work.approvals` + +All examples are visibly marked as mock data. + +`calendar.view` is one stable artifact contract with `day`, `agenda`, and +`week` presentation variants. `widgets.collection` accepts a list that can +combine metric, progress, status, balance, and date widgets in grid, strip, or +stack layouts. + +The production OpenWork Cloud surface still exposes only +`search_capabilities` and `execute_capability`. The same catalog is available +there as the virtual capabilities `openwork.ui_artifacts.search` and +`openwork.ui_artifacts.use`. Successful ordinary `execute_capability` calls +may return a bounded `uiArtifactSuggestions` receipt pointing to the virtual +search capability. This keeps automatic suggestions independent of OpenCode +or any other specific agent engine. + +## Run locally + +```bash +pnpm --filter @openwork/ui-artifact-mcp build +``` + +Or start the TypeScript server directly while developing: + +```bash +pnpm --filter @openwork/ui-artifact-mcp dev +``` + +Add the built server to the worktree's `opencode.json`: + +```json +{ + "mcp": { + "ui-artifacts-demo": { + "type": "local", + "command": [ + "node", + "/absolute/path/to/openwork/packages/ui-artifact-mcp/dist/cli.js" + ], + "enabled": true + } + } +} +``` + +Enable **UI artifacts (Alpha)** in OpenWork Settings → Preferences, then start a +new chat with: + +```text +Use ui-artifacts-demo_search_artifacts to find a workspace brief, then call +ui-artifacts-demo_use_artifact with its exact example arguments. Briefly tell +me what the rendered artifact shows. +``` + +The generic tool view remains the fallback when the alpha preference is off, +the selected artifact kind is disabled, or the returned envelope fails schema +validation. + +## Contract behavior + +- Initial render calls are idempotent. A repeated render returns the retained + current revision instead of resetting the fixture. +- The transcript contains the complete render or replacement payload. +- Approval decisions require an existing rendered `work.approvals` instance, + an exact item ID, an explicit approve/reject choice, and the current expected + revision. Stale and repeated decisions fail closed. +- The demo server accepts only visibly marked mock provenance. Live + provider/account/freshness provenance requires a host-issued receipt. +- Render calls are bound to the artifact ID, version, and canonical schema + digest returned by search; stale or invented definitions are rejected. +- The agent receives a bounded `narration.summary` and `visibleFacts` list, so + the answer remains understandable when the native renderer is unavailable. +- Credential-free HTTPS actions to the fixture's allowlisted Google/Slack hosts + remain navigation-only. Approval buttons stage a minimal agent request and + require normal submission/tool permission; they never mutate directly. + +## Prompt and context policy + +- Suggestions are optional, expire at the end of the current turn, and carry a + dedupe key. The agent renders at most one suggested artifact per turn. +- Suggestion ranking receives capability metadata and argument key presence, + never argument values, provider results, tokens, or credentials. +- After rendering, the agent uses the bounded narration instead of repeating + every visible row or pasting the structured payload. +- Approval prompts include only operation, artifact ID, instance ID, item ID, + decision, and expected revision. They explicitly prohibit calling a real + provider approval capability. diff --git a/packages/ui-artifact-mcp/package.json b/packages/ui-artifact-mcp/package.json new file mode 100644 index 0000000000..d2235f490b --- /dev/null +++ b/packages/ui-artifact-mcp/package.json @@ -0,0 +1,44 @@ +{ + "name": "@openwork/ui-artifact-mcp", + "version": "0.1.0", + "private": true, + "description": "Deterministic local MCP catalog for developing OpenWork UI artifacts", + "type": "module", + "bin": { + "openwork-ui-artifact-mcp": "dist/cli.js" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "bun": "./src/index.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "src", + "README.md" + ], + "scripts": { + "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean", + "dev": "tsx src/cli.ts", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "tsx --test test/*.test.ts", + "check": "pnpm run typecheck && pnpm run test && pnpm run build" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "@openwork/types": "workspace:*", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "tsup": "^8.5.0", + "tsx": "^4.15.7", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/ui-artifact-mcp/src/catalog.ts b/packages/ui-artifact-mcp/src/catalog.ts new file mode 100644 index 0000000000..867101a2c6 --- /dev/null +++ b/packages/ui-artifact-mcp/src/catalog.ts @@ -0,0 +1,373 @@ +import { createHash } from "node:crypto" +import { z } from "zod" +import { + UI_ARTIFACT_KINDS, + UI_ARTIFACT_SCHEMA_VERSION, + uiArtifactApprovalsSchema, + uiArtifactAttentionSchema, + uiArtifactCalendarSchema, + uiArtifactCommunicationThreadSchema, + uiArtifactMailInboxSchema, + uiArtifactPayloadSchema, + uiArtifactSearchInputSchema, + uiArtifactSearchResultSchema, + uiArtifactWidgetsSchema, + uiArtifactWorkspaceBriefSchema, + type UiArtifactErrorCode, + type UiArtifactKind, + type UiArtifactPayload, + type UiArtifactRenderInput, + type UiArtifactSearchInput, + type UiArtifactSearchResult, +} from "@openwork/types/ui-artifact" +import { + APPROVALS_EXAMPLE, + ATTENTION_EXAMPLE, + CALENDAR_EXAMPLE, + COMMUNICATION_THREAD_EXAMPLE, + MAIL_INBOX_EXAMPLE, + WIDGETS_EXAMPLE, + WORKSPACE_BRIEF_EXAMPLE, +} from "./fixtures.js" + +export const searchArtifactsInputSchema = uiArtifactSearchInputSchema +export const searchArtifactsResultSchema = uiArtifactSearchResultSchema +export type SearchArtifactsInput = UiArtifactSearchInput +export type SearchArtifactsResult = UiArtifactSearchResult + +type ArtifactDefinition = { + artifactId: UiArtifactKind + title: string + description: string + keywords: readonly string[] + example: UiArtifactPayload + payloadSchema: z.ZodType +} + +const CATALOG: readonly ArtifactDefinition[] = [ + { + artifactId: "workspace.brief", + title: "Workspace brief", + description: "A complete chat-native work dashboard with metrics, schedule, attention items, progress widgets, and quick actions.", + keywords: ["dashboard", "home", "workspace", "brief", "morning", "today", "overview", "everything", "at a glance", "quick actions"], + example: WORKSPACE_BRIEF_EXAMPLE, + payloadSchema: uiArtifactWorkspaceBriefSchema, + }, + { + artifactId: "calendar.view", + title: "Calendar", + description: "A calendar artifact with day, chronological agenda, and date-grouped week variants.", + keywords: ["calendar", "agenda", "day", "week", "events", "meetings", "schedule", "google calendar", "outlook calendar", "availability", "today"], + example: CALENDAR_EXAMPLE, + payloadSchema: uiArtifactCalendarSchema, + }, + { + artifactId: "widgets.collection", + title: "Widgets", + description: "A composable collection of metric, progress, status, balance, and date widgets in grid, strip, or stack layouts.", + keywords: ["widget", "widgets", "metrics", "progress", "status", "goals", "learning", "payroll", "payslip", "leave", "balance", "summary", "glance"], + example: WIDGETS_EXAMPLE, + payloadSchema: uiArtifactWidgetsSchema, + }, + { + artifactId: "communication.thread", + title: "Conversation thread", + description: "A compact Slack, Teams, or Google Chat thread with participants, messages, reactions, and unread context.", + keywords: ["slack", "teams", "google chat", "conversation", "thread", "channel", "messages", "replies", "chat"], + example: COMMUNICATION_THREAD_EXAMPLE, + payloadSchema: uiArtifactCommunicationThreadSchema, + }, + { + artifactId: "mail.inbox", + title: "Priority inbox", + description: "A Gmail or Outlook inbox preview focused on unread or reply-worthy messages.", + keywords: ["gmail", "outlook", "mail", "email", "inbox", "messages", "replies", "unread", "sender"], + example: MAIL_INBOX_EXAMPLE, + payloadSchema: uiArtifactMailInboxSchema, + }, + { + artifactId: "work.attention", + title: "Attention queue", + description: "A cross-source list of incidents, approvals, tasks, goals, and learning items that need attention.", + keywords: ["attention", "incident", "approval", "task", "goal", "learning", "critical", "overdue", "servicenow", "workday"], + example: ATTENTION_EXAMPLE, + payloadSchema: uiArtifactAttentionSchema, + }, + { + artifactId: "work.approvals", + title: "Approval queue", + description: "A stateful queue of mock approvals with revision-safe approve and reject decisions.", + keywords: ["approval", "approvals", "approve", "reject", "decision", "expense", "access request", "workday", "servicenow"], + example: APPROVALS_EXAMPLE, + payloadSchema: uiArtifactApprovalsSchema, + }, +] + +function normalize(value: string) { + return value.trim().toLocaleLowerCase("en-US") +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(canonicalJson).join(",")}]` + } + if (value && typeof value === "object") { + const entries = Object.entries(value as Record) + .filter(([, child]) => child !== undefined) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`) + return `{${entries.join(",")}}` + } + return JSON.stringify(value) ?? "null" +} + +function schemaDigest(value: Record) { + return `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}` +} + +function renderContract(definition: ArtifactDefinition) { + const payloadJsonSchema = z.toJSONSchema(definition.payloadSchema) as Record + const digest = schemaDigest(payloadJsonSchema) + const inputSchema = z.toJSONSchema(z.object({ + artifactId: z.literal(definition.artifactId), + artifactVersion: z.literal(UI_ARTIFACT_SCHEMA_VERSION), + schemaDigest: z.literal(digest), + artifact: definition.payloadSchema, + }).strict()) as Record + return { digest, inputSchema } +} + +const MOCK_ACTION_HOSTS = new Set([ + "app.slack.com", + "calendar.google.com", + "mail.google.com", +]) + +function unsafeMockAction(value: unknown): string | null { + if (Array.isArray(value)) { + for (const child of value) { + const issue = unsafeMockAction(child) + if (issue) return issue + } + return null + } + if (!value || typeof value !== "object") return null + + const record = value as Record + if (record.type === "open_url" && typeof record.url === "string") { + try { + const url = new URL(record.url) + if ( + url.protocol !== "https:" + || Boolean(url.username) + || Boolean(url.password) + || !MOCK_ACTION_HOSTS.has(url.hostname) + ) { + return `The mock action host is not allowlisted: ${url.hostname || "unknown"}` + } + } catch { + return "The mock action URL is invalid" + } + } + + for (const child of Object.values(record)) { + const issue = unsafeMockAction(child) + if (issue) return issue + } + return null +} + +export type RenderArtifactResolution = + | { ok: true; artifact: UiArtifactPayload } + | { ok: false; code: UiArtifactErrorCode; message: string } + +export function resolveRenderArtifactInput(input: UiArtifactRenderInput): RenderArtifactResolution { + const definition = CATALOG.find((candidate) => candidate.artifactId === input.artifactId) + if (!definition) { + return { ok: false, code: "unknown_artifact", message: `Unknown UI artifact: ${input.artifactId}` } + } + + if (input.artifactVersion !== UI_ARTIFACT_SCHEMA_VERSION) { + return { + ok: false, + code: "renderer_unsupported", + message: `Unsupported ${input.artifactId} artifact version: ${input.artifactVersion}`, + } + } + + const contract = renderContract(definition) + if (input.schemaDigest !== contract.digest) { + return { + ok: false, + code: "schema_digest_mismatch", + message: "The artifact schema changed after discovery. Search artifacts again before rendering.", + } + } + + const payload = definition.payloadSchema.safeParse(input.artifact) + if (!payload.success) { + return { + ok: false, + code: "invalid_artifact_payload", + message: `The payload does not match the searched ${definition.artifactId} schema.`, + } + } + const parsed = uiArtifactPayloadSchema.safeParse(payload.data) + if (!parsed.success) { + return { + ok: false, + code: "invalid_artifact_payload", + message: "The payload is not a supported UI artifact envelope.", + } + } + if (parsed.data.source.type !== "mock") { + return { + ok: false, + code: "source_receipt_required", + message: "The demo MCP accepts only visibly marked mock sources. Live provider data requires a host-issued provenance receipt.", + } + } + if (parsed.data.operation !== "create") { + return { + ok: false, + code: "operation_unsupported", + message: "The demo MCP supports immutable create operations only.", + } + } + if (parsed.data.presentation.placement === "panel") { + return { + ok: false, + code: "renderer_unsupported", + message: "The demo MCP requires inline or both placement because expanded instance rendering is not implemented.", + } + } + const actionIssue = unsafeMockAction(parsed.data) + if (actionIssue) { + return { ok: false, code: "unsafe_action", message: actionIssue } + } + + return { ok: true, artifact: parsed.data } +} + +function tokens(value: string) { + return new Set(normalize(value).match(/[\p{L}\p{N}]+/gu) ?? []) +} + +function boundedArguments(value: Record | undefined) { + if (!value) return "" + try { + return JSON.stringify(value).slice(0, 4_000) + } catch { + return "" + } +} + +function rankDefinition(definition: ArtifactDefinition, input: SearchArtifactsInput) { + const query = normalize(input.query) + const signalText = input.signal + ? [ + input.signal.toolName, + input.signal.toolTitle, + input.signal.toolDescription, + boundedArguments(input.signal.arguments), + ].filter((value): value is string => typeof value === "string").join(" ") + : "" + const haystack = normalize(`${query} ${signalText}`) + const queryTokens = tokens(`${query} ${signalText}`) + const definitionText = normalize([ + definition.artifactId, + definition.title, + definition.description, + ...definition.keywords, + ].join(" ")) + const definitionTokens = tokens(definitionText) + const reasons: string[] = [] + let score = 0 + + if (haystack.includes(normalize(definition.artifactId))) { + score += 40 + reasons.push(`matched ${definition.artifactId}`) + } + + if (haystack.includes(normalize(definition.title))) { + score += 30 + reasons.push(`matched title "${definition.title}"`) + } + + const matchedKeywords = definition.keywords.filter((keyword) => haystack.includes(normalize(keyword))) + if (matchedKeywords.length > 0) { + score += Math.min(60, matchedKeywords.length * 15) + reasons.push(`matched ${matchedKeywords.slice(0, 3).join(", ")}`) + } + + let tokenMatches = 0 + for (const token of queryTokens) { + if (definitionTokens.has(token)) tokenMatches += 1 + } + score += Math.min(30, tokenMatches * 3) + + if (input.signal && score > 0) { + score += 5 + reasons.push(`suggested from ${input.signal.toolName}`) + } + + return { score, reasons } +} + +export function searchArtifacts( + input: SearchArtifactsInput, + options: { transport?: "direct" | "execute_capability" } = {}, +): SearchArtifactsResult { + const transport = options.transport ?? "direct" + const enabled = new Set(input.enabledArtifactIds ?? UI_ARTIFACT_KINDS) + const matches = CATALOG + .filter((definition) => enabled.has(definition.artifactId)) + .map((definition) => { + const ranking = rankDefinition(definition, input) + return { definition, ...ranking } + }) + .filter(({ score }) => score > 0) + .sort((left, right) => right.score - left.score || left.definition.artifactId.localeCompare(right.definition.artifactId)) + .slice(0, input.limit) + .map(({ definition, score, reasons }) => { + const contract = renderContract(definition) + return { + artifactId: definition.artifactId, + title: definition.title, + description: definition.description, + score, + reasons, + toolDefinition: { + name: transport === "execute_capability" ? "execute_capability" as const : "use_artifact" as const, + title: `Render ${definition.title}`, + description: `Render a native OpenWork ${definition.title.toLocaleLowerCase("en-US")} UI artifact in the chat transcript.`, + artifactVersion: UI_ARTIFACT_SCHEMA_VERSION as "1", + schemaDigest: contract.digest, + inputSchema: contract.inputSchema, + invocation: transport === "execute_capability" + ? { + toolName: "execute_capability" as const, + capability: "openwork.ui_artifacts.use" as const, + argumentsField: "body" as const, + } + : { + toolName: "use_artifact" as const, + argumentsField: "artifact" as const, + }, + exampleArguments: { + artifactId: definition.artifactId, + artifactVersion: UI_ARTIFACT_SCHEMA_VERSION as "1", + schemaDigest: contract.digest, + artifact: definition.example, + }, + }, + } + }) + + return { + protocol: "openwork.ui-artifact-search", + schemaVersion: "1", + query: input.query, + matches, + } +} diff --git a/packages/ui-artifact-mcp/src/cli.ts b/packages/ui-artifact-mcp/src/cli.ts new file mode 100644 index 0000000000..f01c7375a9 --- /dev/null +++ b/packages/ui-artifact-mcp/src/cli.ts @@ -0,0 +1,124 @@ +#!/usr/bin/env node + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { + UI_ARTIFACT_MAX_JSON_BYTES, + UI_ARTIFACT_SCHEMA_VERSION, + uiArtifactRenderResultSchema, + uiArtifactUseInputSchema, + type UiArtifactError, + type UiArtifactErrorCode, +} from "@openwork/types/ui-artifact" +import { + searchArtifacts, + searchArtifactsInputSchema, + searchArtifactsResultSchema, +} from "./catalog.js" +import { UiArtifactMockStore } from "./state.js" + +const server = new McpServer({ + name: "openwork-ui-artifacts-demo", + version: "0.1.0", +}) +const store = new UiArtifactMockStore() + +function serializeBounded(value: unknown) { + const text = JSON.stringify(value) + return Buffer.byteLength(text, "utf8") <= UI_ARTIFACT_MAX_JSON_BYTES ? text : null +} + +function oversizedResult() { + return errorResult( + "payload_too_large", + `UI artifact payload exceeds the ${UI_ARTIFACT_MAX_JSON_BYTES}-byte render limit. Reduce rows or excerpt lengths and try again.`, + ) +} + +function errorResult(code: UiArtifactErrorCode, message: string) { + const retry = (() => { + switch (code) { + case "schema_digest_mismatch": + case "manifest_changed": + case "unknown_artifact": + return { action: "search_artifacts" as const, changedArgumentsRequired: true } + case "invalid_artifact_payload": + case "unsafe_action": + case "source_receipt_required": + case "source_receipt_invalid": + case "operation_unsupported": + case "revision_conflict": + case "state_not_found": + case "action_not_allowed": + return { action: "use_artifact" as const, changedArgumentsRequired: true } + default: + return { action: "none" as const, changedArgumentsRequired: false } + } + })() + const error: UiArtifactError = { + protocol: "openwork.ui-artifact-error", + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + code, + message, + retry, + } + return { + content: [{ type: "text" as const, text: serializeBounded(error) ?? JSON.stringify(error) }], + structuredContent: error, + isError: true, + } +} + +server.registerTool( + "search_artifacts", + { + title: "Search UI artifacts", + description: "Find a native OpenWork UI artifact for the user's request. Call this after a data tool returns calendar, email, chat, task, incident, approval, progress, or summary data. Include only bounded triggering tool metadata. The result returns a ranked use_artifact definition and a deterministic demo example. Pick at most one artifact per user turn.", + inputSchema: searchArtifactsInputSchema, + outputSchema: searchArtifactsResultSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + async (input) => { + const result = searchArtifacts(input) + const text = serializeBounded(result) + if (!text) return oversizedResult() + return { + content: [{ type: "text", text }], + structuredContent: result, + } + }, +) + +server.registerTool( + "use_artifact", + { + title: "Use UI artifact", + description: "Render the exact synthetic invocation returned by search_artifacts, or apply an explicit revision-safe decision to a rendered mock approval. The server holds mock state for its process lifetime. Never infer an approval decision. After success, use the narration instead of repeating every row.", + inputSchema: uiArtifactUseInputSchema, + outputSchema: uiArtifactRenderResultSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + async (input) => { + const resolved = store.use(input) + if (!resolved.ok) return errorResult(resolved.code, resolved.message) + const result = uiArtifactRenderResultSchema.parse(resolved.result) + const text = serializeBounded(result) + if (!text) return oversizedResult() + return { + content: [{ type: "text", text }], + structuredContent: result, + } + }, +) + +await server.connect(new StdioServerTransport()) diff --git a/packages/ui-artifact-mcp/src/fixtures.ts b/packages/ui-artifact-mcp/src/fixtures.ts new file mode 100644 index 0000000000..ff390dcee9 --- /dev/null +++ b/packages/ui-artifact-mcp/src/fixtures.ts @@ -0,0 +1,471 @@ +import type { UiArtifactPayload } from "@openwork/types/ui-artifact" + +export const CALENDAR_EXAMPLE = { + artifactId: "calendar.view", + instanceId: "demo-calendar-2026-07-23", + revision: 1, + operation: "create", + title: "Today at a glance", + subtitle: "Thursday, July 23", + presentation: { placement: "inline", size: "standard" }, + source: { + type: "mock", + label: "UI Artifacts demo", + provider: "Google Calendar", + account: "alex@example.com", + observedAt: "2026-07-23T08:30:00+02:00", + }, + data: { + variant: "day", + startDate: "2026-07-23", + endDate: "2026-07-23", + timezone: "Europe/Berlin", + events: [ + { + id: "architecture-review", + title: "Architecture review", + start: "2026-07-23T09:30:00+02:00", + end: "2026-07-23T10:15:00+02:00", + location: "Conference room A", + calendar: "Work", + status: "confirmed", + action: { + id: "open-architecture-review", + label: "View event", + type: "open_url", + url: "https://calendar.google.com/calendar/u/0/r/day", + }, + }, + { + id: "product-sync", + title: "Product sync", + start: "2026-07-23T11:00:00+02:00", + end: "2026-07-23T11:45:00+02:00", + location: "Microsoft Teams", + calendar: "Work", + status: "confirmed", + }, + { + id: "customer-call", + title: "Customer call", + start: "2026-07-23T13:30:00+02:00", + end: "2026-07-23T14:00:00+02:00", + location: "Zoom", + calendar: "Work", + status: "confirmed", + }, + { + id: "performance-check-in", + title: "Performance check-in", + start: "2026-07-23T15:00:00+02:00", + end: "2026-07-23T15:30:00+02:00", + location: "Workday", + calendar: "Work", + status: "tentative", + }, + ], + focusWindow: { + start: "2026-07-23T16:00:00+02:00", + end: "2026-07-23T18:30:00+02:00", + label: "Best focus window", + }, + action: { + id: "open-calendar", + label: "Open calendar", + type: "open_url", + url: "https://calendar.google.com/calendar/u/0/r/day", + }, + }, +} satisfies UiArtifactPayload + +export const COMMUNICATION_THREAD_EXAMPLE = { + artifactId: "communication.thread", + instanceId: "demo-slack-launch-thread", + revision: 1, + operation: "create", + title: "Launch readiness thread", + subtitle: "#product-launch", + presentation: { placement: "inline", size: "standard" }, + source: { + type: "mock", + label: "UI Artifacts demo", + provider: "Slack", + account: "Acme workspace", + observedAt: "2026-07-23T10:04:00+02:00", + }, + data: { + workspace: "Acme", + channel: "product-launch", + topic: "Final checks for the desktop release", + unreadCount: 3, + messages: [ + { + id: "slack-1", + author: "Maya Chen", + timestamp: "2026-07-23T09:48:00+02:00", + body: "The release candidate is signed and the macOS smoke check is green.", + reactions: [{ emoji: "✅", count: 4 }], + }, + { + id: "slack-2", + author: "Theo Martin", + timestamp: "2026-07-23T09:56:00+02:00", + body: "Windows validation is still running. I will post the result before the go/no-go.", + reactions: [{ emoji: "👀", count: 2 }], + }, + { + id: "slack-3", + author: "Priya Shah", + timestamp: "2026-07-23T10:03:00+02:00", + body: "Docs and customer messaging are ready. No remaining content blockers.", + }, + ], + action: { + id: "open-slack", + label: "Open Slack", + type: "open_url", + url: "https://app.slack.com/client", + }, + }, +} satisfies UiArtifactPayload + +export const MAIL_INBOX_EXAMPLE = { + artifactId: "mail.inbox", + instanceId: "demo-gmail-priority-inbox", + revision: 1, + operation: "create", + title: "Priority inbox", + subtitle: "Messages likely needing a reply", + presentation: { placement: "inline", size: "standard" }, + source: { + type: "mock", + label: "UI Artifacts demo", + provider: "Gmail", + account: "alex@example.com", + observedAt: "2026-07-23T10:10:00+02:00", + }, + data: { + account: "alex@example.com", + folder: "Inbox", + unreadCount: 3, + messages: [ + { + id: "mail-1", + sender: "Jordan Lee", + senderEmail: "jordan@example.com", + subject: "Decision needed: launch window", + snippet: "Can you confirm whether we are keeping the 16:00 rollout window?", + receivedAt: "2026-07-23T09:58:00+02:00", + unread: true, + labels: ["Important", "Launch"], + action: { + id: "open-mail-1", + label: "Open", + type: "open_url", + url: "https://mail.google.com/mail/u/0/#inbox", + }, + }, + { + id: "mail-2", + sender: "Finance Ops", + senderEmail: "finance@example.com", + subject: "Expense report returned", + snippet: "One receipt needs a project code before the report can be approved.", + receivedAt: "2026-07-23T08:41:00+02:00", + unread: true, + labels: ["Finance"], + }, + { + id: "mail-3", + sender: "Customer Success", + senderEmail: "success@example.com", + subject: "Notes from the Acme call", + snippet: "The customer confirmed the pilot scope and asked for a follow-up next week.", + receivedAt: "2026-07-22T17:22:00+02:00", + unread: false, + labels: ["Customer"], + }, + ], + action: { + id: "open-gmail", + label: "View inbox", + type: "open_url", + url: "https://mail.google.com/mail/u/0/#inbox", + }, + }, +} satisfies UiArtifactPayload + +export const ATTENTION_EXAMPLE = { + artifactId: "work.attention", + instanceId: "demo-attention-queue", + revision: 1, + operation: "create", + title: "Needs your attention", + subtitle: "Four items across your connected work", + presentation: { placement: "inline", size: "standard" }, + source: { + type: "mock", + label: "UI Artifacts demo", + provider: "OpenWork Connect", + observedAt: "2026-07-23T10:12:00+02:00", + }, + data: { + items: [ + { + id: "incident-payment-latency", + kind: "incident", + title: "P1 payment latency", + description: "Payment service p95 latency is above the incident threshold.", + priority: "critical", + source: "ServiceNow", + dueAt: "2026-07-23T10:30:00+02:00", + }, + { + id: "approval-payroll", + kind: "approval", + title: "Payroll declaration", + description: "Submit the declaration before the July close.", + priority: "high", + source: "Workday", + dueAt: "2026-07-31T17:00:00+02:00", + }, + { + id: "training-security", + kind: "learning", + title: "Security training due", + description: "Complete the annual security refresher.", + priority: "normal", + source: "Learning", + dueAt: "2026-07-24T17:00:00+02:00", + }, + { + id: "goal-update", + kind: "goal", + title: "Goal update requested", + description: "Your manager requested a progress update.", + priority: "normal", + source: "Goals", + }, + ], + }, +} satisfies UiArtifactPayload + +export const WIDGETS_EXAMPLE = { + artifactId: "widgets.collection", + instanceId: "demo-work-widgets", + revision: 1, + operation: "create", + title: "Your widgets", + subtitle: "A combined view across your connected work", + presentation: { placement: "inline", size: "standard" }, + source: { + type: "mock", + label: "UI Artifacts demo", + provider: "OpenWork Connect", + observedAt: "2026-07-23T10:15:00+02:00", + }, + data: { + layout: "grid", + widgets: [ + { + id: "meetings", + kind: "metric", + label: "Meetings", + value: "4", + detail: "Today", + tone: "info", + trend: { direction: "flat", label: "Same as yesterday" }, + }, + { + id: "goals", + kind: "progress", + label: "Q3 goals", + value: "72%", + detail: "On track", + progress: 72, + tone: "success", + }, + { + id: "incident", + kind: "status", + label: "Payment service", + value: "Needs attention", + detail: "P1 latency incident", + status: "attention", + tone: "critical", + }, + { + id: "leave", + kind: "balance", + label: "Leave balance", + value: "14", + unit: "days", + detail: "Available", + tone: "success", + }, + { + id: "payroll", + kind: "date", + label: "Next payslip", + value: "Jul 31", + timestamp: "2026-07-31T09:00:00+02:00", + detail: "8 days", + tone: "neutral", + }, + { + id: "learning", + kind: "progress", + label: "Learning", + value: "2 of 5", + detail: "Courses in progress", + progress: 40, + tone: "info", + }, + ], + }, +} satisfies UiArtifactPayload + +export const APPROVALS_EXAMPLE = { + artifactId: "work.approvals", + instanceId: "demo-approval-queue", + revision: 1, + operation: "create", + title: "Approvals", + subtitle: "Decisions waiting for you", + presentation: { placement: "inline", size: "standard" }, + source: { + type: "mock", + label: "UI Artifacts demo", + provider: "OpenWork mock approvals", + observedAt: "2026-07-23T10:18:00+02:00", + }, + data: { + items: [ + { + id: "expense-lisbon", + title: "Customer workshop travel", + description: "Flights and two hotel nights for the Acme customer workshop in Lisbon.", + requestor: "Maya Chen", + submittedAt: "2026-07-23T08:42:00+02:00", + dueAt: "2026-07-24T17:00:00+02:00", + amount: "€1,280", + source: "Workday", + status: "pending", + actions: [ + { + id: "approve-expense-lisbon", + label: "Approve", + type: "request_decision", + instanceId: "demo-approval-queue", + itemId: "expense-lisbon", + decision: "approve", + expectedRevision: 1, + description: "Stage approval through the agent", + }, + { + id: "reject-expense-lisbon", + label: "Reject", + type: "request_decision", + instanceId: "demo-approval-queue", + itemId: "expense-lisbon", + decision: "reject", + expectedRevision: 1, + description: "Stage rejection through the agent", + }, + ], + }, + { + id: "access-production", + title: "Production dashboard access", + description: "Read-only access for the support rotation.", + requestor: "Theo Martin", + submittedAt: "2026-07-23T09:16:00+02:00", + source: "ServiceNow", + status: "pending", + actions: [ + { + id: "approve-access-production", + label: "Approve", + type: "request_decision", + instanceId: "demo-approval-queue", + itemId: "access-production", + decision: "approve", + expectedRevision: 1, + }, + { + id: "reject-access-production", + label: "Reject", + type: "request_decision", + instanceId: "demo-approval-queue", + itemId: "access-production", + decision: "reject", + expectedRevision: 1, + }, + ], + }, + ], + }, +} satisfies UiArtifactPayload + +export const WORKSPACE_BRIEF_EXAMPLE = { + artifactId: "workspace.brief", + instanceId: "demo-workspace-brief", + revision: 1, + operation: "create", + title: "Good morning, Alex", + subtitle: "Here is what is happening across your work today", + presentation: { placement: "inline", size: "expanded" }, + source: { + type: "mock", + label: "UI Artifacts demo", + provider: "OpenWork Connect", + observedAt: "2026-07-23T10:20:00+02:00", + }, + data: { + summary: "You have four meetings, three messages likely needing a reply, one critical incident, and two approvals waiting.", + metrics: [ + { id: "meetings", label: "Meetings", value: "4", detail: "Today", tone: "info" }, + { id: "replies", label: "Replies", value: "3", detail: "Need response", tone: "warning" }, + { id: "tasks", label: "Tasks", value: "5", detail: "Due today", tone: "success" }, + { id: "critical", label: "Critical", value: "1", detail: "P1 incident", tone: "critical" }, + ], + schedule: CALENDAR_EXAMPLE.data.events, + attention: ATTENTION_EXAMPLE.data.items, + progress: [ + { id: "goals", label: "Q3 goals", value: "72%", detail: "On track", progress: 72, tone: "success" }, + { id: "learning", label: "Learning", value: "2 of 5", detail: "Courses in progress", progress: 40, tone: "info" }, + { id: "payroll", label: "Next payslip", value: "Jul 31", detail: "8 days", tone: "neutral" }, + { id: "leave", label: "Leave balance", value: "14 days", detail: "Available", tone: "success" }, + ], + quickActions: [ + { + id: "open-calendar", + label: "Open calendar", + type: "open_url", + url: "https://calendar.google.com/calendar/u/0/r/day", + }, + { + id: "open-inbox", + label: "Open inbox", + type: "open_url", + url: "https://mail.google.com/mail/u/0/#inbox", + }, + { + id: "open-slack", + label: "Open Slack", + type: "open_url", + url: "https://app.slack.com/client", + }, + ], + }, +} satisfies UiArtifactPayload + +export const UI_ARTIFACT_EXAMPLES = [ + WORKSPACE_BRIEF_EXAMPLE, + CALENDAR_EXAMPLE, + WIDGETS_EXAMPLE, + COMMUNICATION_THREAD_EXAMPLE, + MAIL_INBOX_EXAMPLE, + ATTENTION_EXAMPLE, + APPROVALS_EXAMPLE, +] satisfies readonly UiArtifactPayload[] diff --git a/packages/ui-artifact-mcp/src/index.ts b/packages/ui-artifact-mcp/src/index.ts new file mode 100644 index 0000000000..d5a249ee5e --- /dev/null +++ b/packages/ui-artifact-mcp/src/index.ts @@ -0,0 +1,23 @@ +export { + resolveRenderArtifactInput, + searchArtifacts, + searchArtifactsInputSchema, + searchArtifactsResultSchema, + type SearchArtifactsInput, + type SearchArtifactsResult, +} from "./catalog.js" +export { renderUiArtifact } from "./render.js" +export { + UiArtifactMockStore, + type UiArtifactUseResolution, +} from "./state.js" +export { + APPROVALS_EXAMPLE, + ATTENTION_EXAMPLE, + CALENDAR_EXAMPLE, + COMMUNICATION_THREAD_EXAMPLE, + MAIL_INBOX_EXAMPLE, + UI_ARTIFACT_EXAMPLES, + WIDGETS_EXAMPLE, + WORKSPACE_BRIEF_EXAMPLE, +} from "./fixtures.js" diff --git a/packages/ui-artifact-mcp/src/render.ts b/packages/ui-artifact-mcp/src/render.ts new file mode 100644 index 0000000000..a01e1b56ec --- /dev/null +++ b/packages/ui-artifact-mcp/src/render.ts @@ -0,0 +1,113 @@ +import { + UI_ARTIFACT_PROTOCOL, + UI_ARTIFACT_SCHEMA_VERSION, + type UiArtifactPayload, + type UiArtifactRenderResult, +} from "@openwork/types/ui-artifact" + +function compactFact(value: string) { + const normalized = value.trim() + if (normalized.length <= 160) return normalized + return `${normalized.slice(0, 159).trimEnd()}…` +} + +function calendarNarration(artifact: Extract) { + const count = artifact.data.events.length + const visibleEventLimit = artifact.data.variant === "day" ? 5 : 8 + const visibleFacts = artifact.data.events.slice(0, visibleEventLimit).map((event) => `${event.start}: ${event.title}`) + if (artifact.data.focusWindow) { + visibleFacts.push(`${artifact.data.focusWindow.label}: ${artifact.data.focusWindow.start} to ${artifact.data.focusWindow.end}`) + } + return { + summary: `Rendered the ${artifact.data.variant} calendar variant with ${count} ${count === 1 ? "event" : "events"} from ${artifact.data.startDate} to ${artifact.data.endDate} in ${artifact.data.timezone}.`, + visibleFacts, + } +} + +function threadNarration(artifact: Extract) { + const count = artifact.data.messages.length + const authors = [...new Set(artifact.data.messages.map((message) => message.author))] + return { + summary: `Rendered ${count} ${count === 1 ? "message" : "messages"} from #${artifact.data.channel} in ${artifact.data.workspace}.`, + visibleFacts: [ + `Participants: ${authors.join(", ")}`, + ...artifact.data.messages.slice(0, 4).map((message) => `${message.author}: ${message.body}`), + ], + } +} + +function mailNarration(artifact: Extract) { + const count = artifact.data.messages.length + return { + summary: `Rendered ${count} inbox ${count === 1 ? "message" : "messages"} for ${artifact.data.account}; ${artifact.data.unreadCount} unread.`, + visibleFacts: artifact.data.messages.slice(0, 5).map((message) => `${message.sender}: ${message.subject}`), + } +} + +function attentionNarration(artifact: Extract) { + const count = artifact.data.items.length + const critical = artifact.data.items.filter((item) => item.priority === "critical").length + return { + summary: `Rendered ${count} ${count === 1 ? "item" : "items"} needing attention${critical > 0 ? `, including ${critical} critical` : ""}.`, + visibleFacts: artifact.data.items.slice(0, 5).map((item) => `${item.priority}: ${item.title}`), + } +} + +function widgetsNarration(artifact: Extract) { + const kinds = [...new Set(artifact.data.widgets.map((widget) => widget.kind))] + return { + summary: `Rendered ${artifact.data.widgets.length} combinable widgets in a ${artifact.data.layout} layout: ${kinds.join(", ")}.`, + visibleFacts: artifact.data.widgets.map((widget) => `${widget.label}: ${widget.value}${widget.detail ? ` · ${widget.detail}` : ""}`), + } +} + +function approvalsNarration(artifact: Extract) { + const pending = artifact.data.items.filter((item) => item.status === "pending").length + return { + summary: `Rendered ${artifact.data.items.length} mock approval requests; ${pending} ${pending === 1 ? "is" : "are"} still pending.`, + visibleFacts: artifact.data.items.map((item) => `${item.status}: ${item.title} from ${item.requestor}`), + } +} + +function workspaceBriefNarration(artifact: Extract) { + return { + summary: `Rendered a complete workspace brief with ${artifact.data.metrics.length} metrics, ${artifact.data.schedule.length} events, ${artifact.data.attention.length} attention items, and ${artifact.data.progress.length} progress widgets.`, + visibleFacts: [ + artifact.data.summary, + ...artifact.data.metrics.slice(0, 4).map((metric) => `${metric.label}: ${metric.value}`), + ...artifact.data.attention.slice(0, 2).map((item) => `${item.priority}: ${item.title}`), + ], + } +} + +export function renderUiArtifact(artifact: UiArtifactPayload): UiArtifactRenderResult { + const narration = (() => { + switch (artifact.artifactId) { + case "workspace.brief": + return workspaceBriefNarration(artifact) + case "calendar.view": + return calendarNarration(artifact) + case "widgets.collection": + return widgetsNarration(artifact) + case "communication.thread": + return threadNarration(artifact) + case "mail.inbox": + return mailNarration(artifact) + case "work.attention": + return attentionNarration(artifact) + case "work.approvals": + return approvalsNarration(artifact) + } + })() + + return { + protocol: UI_ARTIFACT_PROTOCOL, + schemaVersion: UI_ARTIFACT_SCHEMA_VERSION, + status: "rendered", + artifact, + narration: { + ...narration, + visibleFacts: narration.visibleFacts.slice(0, 8).map(compactFact), + }, + } +} diff --git a/packages/ui-artifact-mcp/src/state.ts b/packages/ui-artifact-mcp/src/state.ts new file mode 100644 index 0000000000..4f6b3a541c --- /dev/null +++ b/packages/ui-artifact-mcp/src/state.ts @@ -0,0 +1,133 @@ +import { + uiArtifactDecisionInputSchema, + uiArtifactRenderInputSchema, + type UiArtifactErrorCode, + type UiArtifactPayload, + type UiArtifactRenderResult, + type UiArtifactUseInput, +} from "@openwork/types/ui-artifact" +import { resolveRenderArtifactInput } from "./catalog.js" +import { renderUiArtifact } from "./render.js" + +export type UiArtifactUseResolution = + | { ok: true; result: UiArtifactRenderResult } + | { ok: false; code: UiArtifactErrorCode; message: string } + +export class UiArtifactMockStore { + readonly #instances = new Map() + readonly #clock: () => string + + constructor(options: { clock?: () => string } = {}) { + this.#clock = options.clock ?? (() => new Date().toISOString()) + } + + use(input: UiArtifactUseInput): UiArtifactUseResolution { + const decision = uiArtifactDecisionInputSchema.safeParse(input) + if (decision.success) return this.#decide(decision.data) + + const render = uiArtifactRenderInputSchema.safeParse(input) + if (!render.success) { + return { + ok: false, + code: "invalid_artifact_payload", + message: "The UI artifact use input is neither a valid render nor a valid decision.", + } + } + + const resolved = resolveRenderArtifactInput(render.data) + if (!resolved.ok) return resolved + const current = this.#instances.get(resolved.artifact.instanceId) + if (current) return { ok: true, result: renderUiArtifact(current) } + + this.#instances.set(resolved.artifact.instanceId, resolved.artifact) + return { ok: true, result: renderUiArtifact(resolved.artifact) } + } + + snapshot(instanceId: string): UiArtifactPayload | null { + return this.#instances.get(instanceId) ?? null + } + + #decide(input: ReturnType): UiArtifactUseResolution { + const current = this.#instances.get(input.instanceId) + if (!current || current.artifactId !== "work.approvals") { + return { + ok: false, + code: "state_not_found", + message: "Render the approval queue before attempting a decision.", + } + } + if (current.revision !== input.expectedRevision) { + return { + ok: false, + code: "revision_conflict", + message: `The approval queue is now at revision ${current.revision}. Render the current state before deciding.`, + } + } + + const selected = current.data.items.find((item) => item.id === input.itemId) + if (!selected) { + return { + ok: false, + code: "state_not_found", + message: `No approval item named ${input.itemId} exists in this queue.`, + } + } + if (selected.status !== "pending") { + return { + ok: false, + code: "action_not_allowed", + message: `${selected.title} is already ${selected.status}.`, + } + } + + const revision = current.revision + 1 + const decidedAt = this.#clock() + const status: "approved" | "rejected" = input.decision === "approve" ? "approved" : "rejected" + const artifact: UiArtifactPayload = { + ...current, + revision, + operation: "replace", + source: { + ...current.source, + observedAt: decidedAt, + }, + data: { + items: current.data.items.map((item) => { + if (item.id === selected.id) { + return { + ...item, + status, + decidedAt, + ...(input.note ? { decisionNote: input.note } : {}), + actions: undefined, + } + } + if (item.status !== "pending" || !item.actions) return item + return { + ...item, + actions: item.actions.map((action) => ({ + ...action, + expectedRevision: revision, + })), + } + }), + }, + } + this.#instances.set(artifact.instanceId, artifact) + + const result = renderUiArtifact(artifact) + return { + ok: true, + result: { + ...result, + interaction: { + type: "decision", + itemId: input.itemId, + decision: input.decision, + previousRevision: current.revision, + revision, + }, + }, + } + } +} diff --git a/packages/ui-artifact-mcp/test/catalog.test.ts b/packages/ui-artifact-mcp/test/catalog.test.ts new file mode 100644 index 0000000000..32f53e7de7 --- /dev/null +++ b/packages/ui-artifact-mcp/test/catalog.test.ts @@ -0,0 +1,259 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { + UI_ARTIFACT_KINDS, + UI_ARTIFACT_USE_CAPABILITY, + uiArtifactActionSchema, + uiArtifactCalendarSchema, + uiArtifactRenderInputSchema, + uiArtifactRenderResultSchema, + uiArtifactWidgetsSchema, +} from "@openwork/types/ui-artifact" +import { + CALENDAR_EXAMPLE, + COMMUNICATION_THREAD_EXAMPLE, + UiArtifactMockStore, + WIDGETS_EXAMPLE, + resolveRenderArtifactInput, + searchArtifacts, + searchArtifactsInputSchema, + renderUiArtifact, +} from "../src/index.js" + +test("calendar tool metadata ranks the calendar artifact first", () => { + const input = searchArtifactsInputSchema.parse({ + query: "show me the day", + signal: { + toolName: "google_calendar_list_events", + toolTitle: "List calendar events", + arguments: { date: "2026-07-23" }, + }, + }) + + const result = searchArtifacts(input) + assert.deepEqual(searchArtifacts(input), result) + assert.equal(result.matches[0]?.artifactId, "calendar.view") + assert.equal(result.matches[0]?.toolDefinition.name, "use_artifact") + assert.equal(result.matches[0]?.toolDefinition.artifactVersion, "1") + assert.equal( + result.matches[0]?.toolDefinition.schemaDigest, + "sha256:f93d4c65c4352c6857d29bd8c9826b2ced7f4b8561e37ebfc05d2da50c33fe83", + ) + assert.deepEqual(result.matches[0]?.toolDefinition.invocation, { + toolName: "use_artifact", + argumentsField: "artifact", + }) + assert.equal( + uiArtifactRenderInputSchema.safeParse(result.matches[0]?.toolDefinition.exampleArguments).success, + true, + ) +}) + +test("enabled artifact filtering is deterministic", () => { + const input = searchArtifactsInputSchema.parse({ + query: "show email and slack messages", + enabledArtifactIds: ["mail.inbox"], + limit: 5, + }) + + const result = searchArtifacts(input) + assert.deepEqual(result.matches.map((match) => match.artifactId), ["mail.inbox"]) +}) + +test("capability transport returns an execute_capability use definition", () => { + const input = searchArtifactsInputSchema.parse({ + query: "calendar events today", + limit: 1, + }) + const result = searchArtifacts(input, { transport: "execute_capability" }) + assert.equal(result.matches[0]?.toolDefinition.name, "execute_capability") + assert.deepEqual(result.matches[0]?.toolDefinition.invocation, { + toolName: "execute_capability", + capability: UI_ARTIFACT_USE_CAPABILITY, + argumentsField: "body", + }) +}) + +test("every catalog kind can be enabled without an unknown value", () => { + assert.deepEqual([...UI_ARTIFACT_KINDS].sort(), [ + "calendar.view", + "communication.thread", + "mail.inbox", + "widgets.collection", + "work.approvals", + "work.attention", + "workspace.brief", + ]) +}) + +test("calendar is one artifact with day, agenda, and week variants", () => { + for (const variant of ["day", "agenda", "week"] as const) { + const artifact = { + ...CALENDAR_EXAMPLE, + data: { + ...CALENDAR_EXAMPLE.data, + variant, + }, + } + assert.equal(uiArtifactCalendarSchema.safeParse(artifact).success, true) + } +}) + +test("widgets combine heterogeneous widget kinds in one collection", () => { + const parsed = uiArtifactWidgetsSchema.parse(WIDGETS_EXAMPLE) + assert.deepEqual( + [...new Set(parsed.data.widgets.map((widget) => widget.kind))].sort(), + ["balance", "date", "metric", "progress", "status"], + ) + assert.equal(parsed.data.layout, "grid") +}) + +test("stateful approval decisions are revision-safe and update later actions", () => { + const search = searchArtifacts(searchArtifactsInputSchema.parse({ + query: "approvals awaiting my decision", + limit: 1, + })) + const input = search.matches[0]?.toolDefinition.exampleArguments + assert.ok(input) + assert.equal(input.artifactId, "work.approvals") + + const store = new UiArtifactMockStore({ + clock: () => "2026-07-23T11:00:00.000Z", + }) + const initial = store.use(input) + assert.equal(initial.ok, true) + if (!initial.ok) return + assert.equal(initial.result.artifact.artifactId, "work.approvals") + + const decided = store.use({ + operation: "decide", + artifactId: "work.approvals", + instanceId: initial.result.artifact.instanceId, + itemId: "expense-lisbon", + decision: "approve", + expectedRevision: 1, + }) + assert.equal(decided.ok, true) + if (!decided.ok || decided.result.artifact.artifactId !== "work.approvals") return + assert.equal(decided.result.artifact.revision, 2) + assert.equal(decided.result.artifact.operation, "replace") + assert.equal(decided.result.artifact.data.items[0]?.status, "approved") + assert.equal(decided.result.artifact.data.items[1]?.actions?.[0]?.expectedRevision, 2) + assert.deepEqual(decided.result.interaction, { + type: "decision", + itemId: "expense-lisbon", + decision: "approve", + previousRevision: 1, + revision: 2, + }) + + const stale = store.use({ + operation: "decide", + artifactId: "work.approvals", + instanceId: initial.result.artifact.instanceId, + itemId: "access-production", + decision: "reject", + expectedRevision: 1, + }) + assert.equal(stale.ok, false) + if (!stale.ok) assert.equal(stale.code, "revision_conflict") +}) + +test("render result validates the shared contract and narrates visible data", () => { + const result = renderUiArtifact(CALENDAR_EXAMPLE) + assert.equal(uiArtifactRenderResultSchema.safeParse(result).success, true) + assert.match(result.narration.summary, /day calendar variant with 4 events/) + assert.equal(result.artifact.instanceId, "demo-calendar-2026-07-23") +}) + +test("narration facts stay inside the compact contract for long valid messages", () => { + const artifact = { + ...COMMUNICATION_THREAD_EXAMPLE, + data: { + ...COMMUNICATION_THREAD_EXAMPLE.data, + messages: [{ + ...COMMUNICATION_THREAD_EXAMPLE.data.messages[0], + body: "x".repeat(2_000), + }], + }, + } + const result = renderUiArtifact(artifact) + assert.equal(uiArtifactRenderResultSchema.safeParse(result).success, true) + assert.equal(result.narration.visibleFacts.every((fact) => fact.length <= 160), true) +}) + +test("artifact actions accept only credential-free https URLs", () => { + const base = { id: "open", label: "Open", type: "open_url" as const } + assert.equal(uiArtifactActionSchema.safeParse({ ...base, url: "https://example.com" }).success, true) + assert.equal(uiArtifactActionSchema.safeParse({ ...base, url: "http://example.com" }).success, false) + assert.equal(uiArtifactActionSchema.safeParse({ ...base, url: "javascript:alert(1)" }).success, false) + assert.equal(uiArtifactActionSchema.safeParse({ ...base, url: "file:///tmp/private" }).success, false) + assert.equal(uiArtifactActionSchema.safeParse({ ...base, url: "https://user:secret@example.com" }).success, false) +}) + +test("render input is bound to the searched schema and mock alpha capabilities", () => { + const search = searchArtifacts(searchArtifactsInputSchema.parse({ + query: "calendar today", + limit: 1, + })) + const input = search.matches[0]?.toolDefinition.exampleArguments + assert.ok(input) + assert.equal(resolveRenderArtifactInput(input).ok, true) + + const wrongDigest = { + ...input, + schemaDigest: `sha256:${"0".repeat(64)}`, + } + const digestResult = resolveRenderArtifactInput(wrongDigest) + assert.equal(digestResult.ok, false) + if (!digestResult.ok) assert.equal(digestResult.code, "schema_digest_mismatch") + + const artifact = uiArtifactCalendarSchema.parse(input.artifact) + const replaceResult = resolveRenderArtifactInput({ + ...input, + artifact: { ...artifact, operation: "replace" }, + }) + assert.equal(replaceResult.ok, false) + if (!replaceResult.ok) assert.equal(replaceResult.code, "operation_unsupported") + + const panelResult = resolveRenderArtifactInput({ + ...input, + artifact: { + ...artifact, + presentation: { ...artifact.presentation, placement: "panel" }, + }, + }) + assert.equal(panelResult.ok, false) + if (!panelResult.ok) assert.equal(panelResult.code, "renderer_unsupported") + + const providerResult = resolveRenderArtifactInput({ + ...input, + artifact: { + ...artifact, + source: { ...artifact.source, type: "provider" }, + }, + }) + assert.equal(providerResult.ok, false) + if (!providerResult.ok) assert.equal(providerResult.code, "source_receipt_required") + + const unsafeActionResult = resolveRenderArtifactInput({ + ...input, + artifact: { + ...artifact, + data: { + ...artifact.data, + events: [{ + ...artifact.data.events[0], + action: { + id: "unsafe", + label: "Open", + type: "open_url", + url: "https://untrusted.example/path", + }, + }], + }, + }, + }) + assert.equal(unsafeActionResult.ok, false) + if (!unsafeActionResult.ok) assert.equal(unsafeActionResult.code, "unsafe_action") +}) diff --git a/packages/ui-artifact-mcp/test/stdio.test.ts b/packages/ui-artifact-mcp/test/stdio.test.ts new file mode 100644 index 0000000000..8e08f45a78 --- /dev/null +++ b/packages/ui-artifact-mcp/test/stdio.test.ts @@ -0,0 +1,66 @@ +import assert from "node:assert/strict" +import { fileURLToPath } from "node:url" +import test from "node:test" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" +import { + uiArtifactRenderResultSchema, + uiArtifactSearchResultSchema, +} from "@openwork/types/ui-artifact" + +test("startable stdio MCP exposes two tools and retains approval state", { timeout: 30_000 }, async () => { + const cli = fileURLToPath(new URL("../src/cli.ts", import.meta.url)) + const cwd = fileURLToPath(new URL("..", import.meta.url)) + const transport = new StdioClientTransport({ + command: process.execPath, + args: ["--import", "tsx", cli], + cwd, + stderr: "pipe", + }) + const client = new Client({ name: "ui-artifact-mcp-test", version: "1.0.0" }) + + try { + await client.connect(transport) + const tools = await client.listTools() + assert.deepEqual(tools.tools.map((tool) => tool.name).sort(), ["search_artifacts", "use_artifact"]) + + const searched = await client.callTool({ + name: "search_artifacts", + arguments: { query: "approvals awaiting my decision", limit: 1 }, + }) + const search = uiArtifactSearchResultSchema.parse(searched.structuredContent) + const match = search.matches[0] + assert.equal(match?.artifactId, "work.approvals") + + const rendered = await client.callTool({ + name: "use_artifact", + arguments: match?.toolDefinition.exampleArguments, + }) + const initial = uiArtifactRenderResultSchema.parse(rendered.structuredContent) + assert.equal(initial.artifact.revision, 1) + + const decided = await client.callTool({ + name: "use_artifact", + arguments: { + operation: "decide", + artifactId: "work.approvals", + instanceId: initial.artifact.instanceId, + itemId: "expense-lisbon", + decision: "approve", + expectedRevision: 1, + }, + }) + const updated = uiArtifactRenderResultSchema.parse(decided.structuredContent) + assert.equal(updated.artifact.revision, 2) + assert.equal(updated.interaction?.decision, "approve") + + const rerendered = await client.callTool({ + name: "use_artifact", + arguments: match?.toolDefinition.exampleArguments, + }) + const current = uiArtifactRenderResultSchema.parse(rerendered.structuredContent) + assert.equal(current.artifact.revision, 2) + } finally { + await client.close() + } +}) diff --git a/packages/ui-artifact-mcp/tsconfig.json b/packages/ui-artifact-mcp/tsconfig.json new file mode 100644 index 0000000000..b0456765bb --- /dev/null +++ b/packages/ui-artifact-mcp/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "types": ["node"] + }, + "include": ["src", "test"] +} diff --git a/packaging/docker/Dockerfile.den b/packaging/docker/Dockerfile.den index d7506b715c..98d347b2c9 100644 --- a/packaging/docker/Dockerfile.den +++ b/packaging/docker/Dockerfile.den @@ -16,6 +16,7 @@ COPY packages/email/package.json /app/packages/email/package.json COPY packages/enterprise-mcp-client/package.json /app/packages/enterprise-mcp-client/package.json COPY packages/install-config/package.json /app/packages/install-config/package.json COPY packages/connect-link/package.json /app/packages/connect-link/package.json +COPY packages/ui-artifact-mcp/package.json /app/packages/ui-artifact-mcp/package.json # apps/desktop/package.json is copied below for version metadata, and it depends # on @openwork/paths, so the package must exist for workspace resolution. COPY packages/paths/package.json /app/packages/paths/package.json @@ -37,6 +38,7 @@ COPY packages/email /app/packages/email COPY packages/enterprise-mcp-client /app/packages/enterprise-mcp-client COPY packages/install-config /app/packages/install-config COPY packages/connect-link /app/packages/connect-link +COPY packages/ui-artifact-mcp /app/packages/ui-artifact-mcp COPY packages/paths /app/packages/paths COPY ee/packages/utils /app/ee/packages/utils COPY ee/packages/den-db /app/ee/packages/den-db @@ -47,6 +49,7 @@ RUN pnpm --dir /app/packages/email run build RUN pnpm --dir /app/packages/enterprise-mcp-client run build RUN pnpm --dir /app/packages/install-config run build RUN pnpm --dir /app/packages/connect-link run build +RUN pnpm --dir /app/packages/ui-artifact-mcp run build RUN pnpm --dir /app/ee/packages/utils run build RUN pnpm --dir /app/ee/packages/den-db run build # Source-map upload is build-only; runtime Sentry still requires its own backend and DSN configuration. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea6227c4da..5bbd50462a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -429,6 +429,9 @@ importers: '@openwork/types': specifier: workspace:* version: link:../../../packages/types + '@openwork/ui-artifact-mcp': + specifier: workspace:* + version: link:../../../packages/ui-artifact-mcp '@sentry/hono': specifier: 10.64.0 version: 10.64.0(@hono/node-server@1.19.11(hono@4.12.8))(@sentry/node@10.64.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.220.0(@opentelemetry/api@1.9.1)))(hono@4.12.8) @@ -580,7 +583,7 @@ importers: version: 0.0.72(@types/react@19.2.14)(react@19.2.4) '@sentry/nextjs': specifier: 10.64.0 - version: 10.64.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.2.1(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.4(postcss@8.4.38)) + version: 10.64.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.4(postcss@8.4.38)) '@tanstack/react-query': specifier: ^5.96.2 version: 5.96.2(react@19.2.4) @@ -589,7 +592,7 @@ importers: version: 0.577.0(react@19.2.4) next: specifier: 16.2.1 - version: 16.2.1(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 'catalog:' version: 19.2.4 @@ -660,7 +663,7 @@ importers: version: link:../../../packages/types next: specifier: 16.2.1 - version: 16.2.1(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 'catalog:' version: 19.2.4 @@ -1045,6 +1048,31 @@ importers: specifier: ^5.6.3 version: 5.9.3 + packages/ui-artifact-mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(patch_hash=2038e94bf511d9f917a4315c9f9376716a97f7f09cd830e99d9edd751e244bff)(zod@4.3.6) + '@openwork/types': + specifier: workspace:* + version: link:../types + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.13.3 + tsup: + specifier: ^8.5.0 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + tsx: + specifier: ^4.15.7 + version: 4.21.0 + typescript: + specifier: ^5.6.3 + version: 5.9.3 + packages: '@ai-sdk/gateway@3.0.88': @@ -12282,7 +12310,7 @@ snapshots: '@hono/node-server': 1.19.11(hono@4.12.12) '@sentry/node': 10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.220.0(@opentelemetry/api@1.9.1)) - '@sentry/nextjs@10.64.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.2.1(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.4(postcss@8.4.38))': + '@sentry/nextjs@10.64.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.4(postcss@8.4.38))': dependencies: '@opentelemetry/api': 1.9.1 '@rollup/plugin-commonjs': 28.0.1(rollup@4.62.2) @@ -12295,7 +12323,7 @@ snapshots: '@sentry/react': 10.64.0(react@19.2.4) '@sentry/vercel-edge': 10.64.0 '@sentry/webpack-plugin': 5.4.0(rollup@4.62.2)(webpack@5.108.4(postcss@8.4.38)) - next: 16.2.1(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) rollup: 4.62.2 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -15694,7 +15722,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.2.1(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.1 '@swc/helpers': 0.5.15 @@ -15703,7 +15731,7 @@ snapshots: postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(react@19.2.4) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) optionalDependencies: '@next/swc-darwin-arm64': 16.2.1 '@next/swc-darwin-x64': 16.2.1 @@ -15729,7 +15757,7 @@ snapshots: postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(react@19.2.4) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) optionalDependencies: '@next/swc-darwin-arm64': 16.2.3 '@next/swc-darwin-x64': 16.2.3 @@ -16868,10 +16896,12 @@ snapshots: client-only: 0.0.1 react: 18.2.0 - styled-jsx@5.1.6(react@19.2.4): + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): dependencies: client-only: 0.0.1 react: 19.2.4 + optionalDependencies: + '@babel/core': 7.29.0 sucrase@3.35.1: dependencies: